├── .github └── workflows │ ├── ghcr.yml │ └── pypi_ghcr.yml ├── .gitignore ├── CHANGELOG.md ├── LICENSE ├── README.md ├── docker ├── Dockerfile ├── README.md └── docker-compose.yml ├── requirements.txt ├── setup.cfg ├── setup.py └── vbmc4vsphere ├── __init__.py ├── cmd ├── __init__.py ├── vsbmc.py └── vsbmcd.py ├── config.py ├── control.py ├── exception.py ├── log.py ├── manager.py ├── utils.py └── vbmc.py /.github/workflows/ghcr.yml: -------------------------------------------------------------------------------- 1 | name: GitHub Container Registry 2 | 3 | on: 4 | workflow_dispatch: 5 | inputs: 6 | ref: 7 | description: "The branch, tag or SHA to checkout" 8 | required: true 9 | default: "0.0.0" 10 | tag: 11 | description: "The new tag for the container image" 12 | required: true 13 | default: "0.0.0" 14 | 15 | jobs: 16 | ghcr: 17 | runs-on: ubuntu-22.04 18 | 19 | steps: 20 | - name: Checkout git repository 21 | uses: actions/checkout@v2 22 | with: 23 | ref: ${{ github.event.inputs.ref }} 24 | 25 | - name: Set up Docker Buildx 26 | uses: docker/setup-buildx-action@v1 27 | 28 | - name: Login to GitHub Container Registry 29 | uses: docker/login-action@v1 30 | with: 31 | registry: ghcr.io 32 | username: ${{ github.repository_owner }} 33 | password: ${{ secrets.GITHUB_TOKEN }} 34 | 35 | - name: Build and push 36 | uses: docker/build-push-action@v2 37 | with: 38 | context: ./docker/. 39 | push: true 40 | tags: ghcr.io/${{ github.repository_owner }}/vbmc4vsphere:${{ github.event.inputs.tag }} 41 | -------------------------------------------------------------------------------- /.github/workflows/pypi_ghcr.yml: -------------------------------------------------------------------------------- 1 | name: TestPyPI, PyPI, and GitHub Container Registry 2 | 3 | on: push 4 | 5 | jobs: 6 | pypi: 7 | runs-on: ubuntu-22.04 8 | 9 | steps: 10 | - name: Checkout git repository 11 | uses: actions/checkout@v2 12 | with: 13 | fetch-depth: 0 14 | 15 | - name: Set up Python 16 | uses: actions/setup-python@v1 17 | with: 18 | python-version: 3.x 19 | 20 | - name: Install dependencies 21 | run: | 22 | python -m pip install --upgrade pip 23 | python -m pip install build --user 24 | 25 | - name: Build a binary wheel and a source tarball 26 | run: | 27 | python -m build --sdist --wheel --outdir dist/ . 28 | 29 | - name: Publish a Python distribution to Test PyPI 30 | if: startsWith(github.ref, 'refs/tags') != true 31 | uses: pypa/gh-action-pypi-publish@release/v1 32 | with: 33 | user: __token__ 34 | password: ${{ secrets.TEST_PYPI_API_TOKEN }} 35 | repository_url: https://test.pypi.org/legacy/ 36 | 37 | - name: Publish a Python distribution to Production PyPI 38 | if: startsWith(github.ref, 'refs/tags') 39 | uses: pypa/gh-action-pypi-publish@release/v1 40 | with: 41 | user: __token__ 42 | password: ${{ secrets.PYPI_API_TOKEN }} 43 | 44 | ghcr: 45 | if: startsWith(github.ref, 'refs/tags') 46 | runs-on: ubuntu-22.04 47 | needs: pypi 48 | 49 | steps: 50 | - name: Checkout git repository 51 | uses: actions/checkout@v2 52 | 53 | - name: Get version from tag 54 | id: vars 55 | run: echo ::set-output name=tag::${GITHUB_REF#refs/*/} 56 | 57 | - name: Set up Docker Buildx 58 | uses: docker/setup-buildx-action@v1 59 | 60 | - name: Login to GitHub Container Registry 61 | uses: docker/login-action@v1 62 | with: 63 | registry: ghcr.io 64 | username: ${{ github.repository_owner }} 65 | password: ${{ secrets.GITHUB_TOKEN }} 66 | 67 | - name: Build and push 68 | uses: docker/build-push-action@v2 69 | with: 70 | context: ./docker/. 71 | push: true 72 | tags: ghcr.io/${{ github.repository_owner }}/vbmc4vsphere:${{ steps.vars.outputs.tag }} 73 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.py[cod] 2 | __pycache__ 3 | 4 | # C extensions 5 | *.so 6 | 7 | # Packages 8 | *.egg 9 | *.egg-info 10 | dist 11 | build 12 | .eggs 13 | eggs 14 | parts 15 | bin 16 | var 17 | sdist 18 | develop-eggs 19 | .installed.cfg 20 | lib 21 | lib64 22 | 23 | # Installer logs 24 | pip-log.txt 25 | 26 | # Unit test / coverage reports 27 | .coverage 28 | cover 29 | .tox 30 | nosetests.xml 31 | .testrepository 32 | .stestr 33 | .venv 34 | 35 | # Translations 36 | *.mo 37 | 38 | # Files created by releasenotes build 39 | releasenotes/build 40 | 41 | # Mr Developer 42 | .mr.developer.cfg 43 | .project 44 | .pydevproject 45 | 46 | # Complexity 47 | output/*.html 48 | output/*/index.html 49 | 50 | # Sphinx 51 | doc/build 52 | 53 | # pbr generates these 54 | AUTHORS 55 | ChangeLog 56 | 57 | # Editors 58 | *~ 59 | .*.swp 60 | .*sw? 61 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Change Log 2 | 3 | All notable changes to this project will be documented in this file. 4 | 5 | The format is based on [Keep a Changelog](http://keepachangelog.com/) 6 | and this project adheres to [Semantic Versioning](http://semver.org/). 7 | 8 | ## [Unreleased] 9 | 10 | ### Added 11 | 12 | - N/A 13 | 14 | ## [0.3.0] - 2022-10-01 15 | 16 | ### Added 17 | 18 | - Add `--vm-uuid` option to `vsbmc add` command to specify UUID for virtual machine without modifying config file (#17) 19 | 20 | ## [0.2.0] - 2022-10-01 21 | 22 | ### Added 23 | 24 | - Add UUID lookup method via config file for VM search (#14) 25 | 26 | ### Changed 27 | 28 | - Bump Python version to 3.10 29 | 30 | ## [0.1.0] - 2022-02-02 31 | 32 | ### Breaking Changes 33 | 34 | - Rename commands to `vsbmc` and `vsbmcd` to allow coexistence with the original VirtualBMC (#9) 35 | 36 | ### Fixed 37 | 38 | - Make the container image to run as non-root user (#11) 39 | 40 | ## [0.0.8] - 2021-09-15 41 | 42 | ### Fixed 43 | 44 | - Failed to find VM in environments with more than 100 VMs (#5) 45 | 46 | ## [0.0.7] - 2021-03-27 47 | 48 | ### Fixed 49 | 50 | - Failed to pickle `vbmc_runner` due to `multiprocessing` on Python 3.8+ on macOS (#2) 51 | 52 | ## [0.0.6] - 2020-09-11 53 | 54 | ### Added 55 | 56 | - Add experimental support for `chassis power diag` 57 | 58 | ## [0.0.5] - 2020-09-09 59 | 60 | ### Added 61 | 62 | - Add `chassis bootdev pxe|disk|cdrom` command support 63 | - Add `chassis bootparam get 5` command support 64 | 65 | ## [0.0.4] - 2020-09-06 66 | 67 | ### Added 68 | 69 | - Add docker support 70 | 71 | ## [0.0.3] - 2020-09-05 72 | 73 | ### Added 74 | 75 | - Add "Get Channel Information" command support with faked response 76 | - Add "Get Channel Access" command support with faked response 77 | - Add "Get LAN Configuration Parameters" command with faked response 78 | - Add ability to control fake MAC address to pass the sanity check of vCenter Server 79 | 80 | ## [0.0.2] - 2020-09-04 81 | 82 | ### Added 83 | 84 | - Patch pyghmi to support `0x38` command in IPMI v2.0 85 | - Add ASF Presence Ping/Pong support 86 | - Add [CHANGELOG.md] 87 | 88 | ### Changed 89 | 90 | - Refactor some codes 91 | - Update [README.md] 92 | 93 | ## 0.0.1 - 2020-08-31 94 | 95 | ### Added 96 | 97 | - Add VMware vSphere support with few IPMI commands and remove OpenStack support 98 | - Project starts based on the copy of [VirtualBMC 2.1.0.dev](https://github.com/openstack/virtualbmc/commit/c4c8edb66bc49fcb1b8fb41af77546e06d2e8bce) 99 | 100 | [Unreleased]: https://github.com/kurokobo/virtualbmc-for-vsphere/compare/0.3.0...HEAD 101 | [0.3.0]: https://github.com/kurokobo/virtualbmc-for-vsphere/compare/0.2.0...0.3.0 102 | [0.2.0]: https://github.com/kurokobo/virtualbmc-for-vsphere/compare/0.1.0...0.2.0 103 | [0.1.0]: https://github.com/kurokobo/virtualbmc-for-vsphere/compare/0.0.8...0.1.0 104 | [0.0.8]: https://github.com/kurokobo/virtualbmc-for-vsphere/compare/0.0.7...0.0.8 105 | [0.0.7]: https://github.com/kurokobo/virtualbmc-for-vsphere/compare/0.0.6...0.0.7 106 | [0.0.6]: https://github.com/kurokobo/virtualbmc-for-vsphere/compare/0.0.5...0.0.6 107 | [0.0.5]: https://github.com/kurokobo/virtualbmc-for-vsphere/compare/0.0.4...0.0.5 108 | [0.0.4]: https://github.com/kurokobo/virtualbmc-for-vsphere/compare/0.0.3...0.0.4 109 | [0.0.3]: https://github.com/kurokobo/virtualbmc-for-vsphere/compare/0.0.2...0.0.3 110 | [0.0.2]: https://github.com/kurokobo/virtualbmc-for-vsphere/compare/0.0.1...0.0.2 111 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | 2 | Apache License 3 | Version 2.0, January 2004 4 | http://www.apache.org/licenses/ 5 | 6 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 7 | 8 | 1. Definitions. 9 | 10 | "License" shall mean the terms and conditions for use, reproduction, 11 | and distribution as defined by Sections 1 through 9 of this document. 12 | 13 | "Licensor" shall mean the copyright owner or entity authorized by 14 | the copyright owner that is granting the License. 15 | 16 | "Legal Entity" shall mean the union of the acting entity and all 17 | other entities that control, are controlled by, or are under common 18 | control with that entity. For the purposes of this definition, 19 | "control" means (i) the power, direct or indirect, to cause the 20 | direction or management of such entity, whether by contract or 21 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 22 | outstanding shares, or (iii) beneficial ownership of such entity. 23 | 24 | "You" (or "Your") shall mean an individual or Legal Entity 25 | exercising permissions granted by this License. 26 | 27 | "Source" form shall mean the preferred form for making modifications, 28 | including but not limited to software source code, documentation 29 | source, and configuration files. 30 | 31 | "Object" form shall mean any form resulting from mechanical 32 | transformation or translation of a Source form, including but 33 | not limited to compiled object code, generated documentation, 34 | and conversions to other media types. 35 | 36 | "Work" shall mean the work of authorship, whether in Source or 37 | Object form, made available under the License, as indicated by a 38 | copyright notice that is included in or attached to the work 39 | (an example is provided in the Appendix below). 40 | 41 | "Derivative Works" shall mean any work, whether in Source or Object 42 | form, that is based on (or derived from) the Work and for which the 43 | editorial revisions, annotations, elaborations, or other modifications 44 | represent, as a whole, an original work of authorship. For the purposes 45 | of this License, Derivative Works shall not include works that remain 46 | separable from, or merely link (or bind by name) to the interfaces of, 47 | the Work and Derivative Works thereof. 48 | 49 | "Contribution" shall mean any work of authorship, including 50 | the original version of the Work and any modifications or additions 51 | to that Work or Derivative Works thereof, that is intentionally 52 | submitted to Licensor for inclusion in the Work by the copyright owner 53 | or by an individual or Legal Entity authorized to submit on behalf of 54 | the copyright owner. For the purposes of this definition, "submitted" 55 | means any form of electronic, verbal, or written communication sent 56 | to the Licensor or its representatives, including but not limited to 57 | communication on electronic mailing lists, source code control systems, 58 | and issue tracking systems that are managed by, or on behalf of, the 59 | Licensor for the purpose of discussing and improving the Work, but 60 | excluding communication that is conspicuously marked or otherwise 61 | designated in writing by the copyright owner as "Not a Contribution." 62 | 63 | "Contributor" shall mean Licensor and any individual or Legal Entity 64 | on behalf of whom a Contribution has been received by Licensor and 65 | subsequently incorporated within the Work. 66 | 67 | 2. Grant of Copyright License. Subject to the terms and conditions of 68 | this License, each Contributor hereby grants to You a perpetual, 69 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 70 | copyright license to reproduce, prepare Derivative Works of, 71 | publicly display, publicly perform, sublicense, and distribute the 72 | Work and such Derivative Works in Source or Object form. 73 | 74 | 3. Grant of Patent License. Subject to the terms and conditions of 75 | this License, each Contributor hereby grants to You a perpetual, 76 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 77 | (except as stated in this section) patent license to make, have made, 78 | use, offer to sell, sell, import, and otherwise transfer the Work, 79 | where such license applies only to those patent claims licensable 80 | by such Contributor that are necessarily infringed by their 81 | Contribution(s) alone or by combination of their Contribution(s) 82 | with the Work to which such Contribution(s) was submitted. If You 83 | institute patent litigation against any entity (including a 84 | cross-claim or counterclaim in a lawsuit) alleging that the Work 85 | or a Contribution incorporated within the Work constitutes direct 86 | or contributory patent infringement, then any patent licenses 87 | granted to You under this License for that Work shall terminate 88 | as of the date such litigation is filed. 89 | 90 | 4. Redistribution. You may reproduce and distribute copies of the 91 | Work or Derivative Works thereof in any medium, with or without 92 | modifications, and in Source or Object form, provided that You 93 | meet the following conditions: 94 | 95 | (a) You must give any other recipients of the Work or 96 | Derivative Works a copy of this License; and 97 | 98 | (b) You must cause any modified files to carry prominent notices 99 | stating that You changed the files; and 100 | 101 | (c) You must retain, in the Source form of any Derivative Works 102 | that You distribute, all copyright, patent, trademark, and 103 | attribution notices from the Source form of the Work, 104 | excluding those notices that do not pertain to any part of 105 | the Derivative Works; and 106 | 107 | (d) If the Work includes a "NOTICE" text file as part of its 108 | distribution, then any Derivative Works that You distribute must 109 | include a readable copy of the attribution notices contained 110 | within such NOTICE file, excluding those notices that do not 111 | pertain to any part of the Derivative Works, in at least one 112 | of the following places: within a NOTICE text file distributed 113 | as part of the Derivative Works; within the Source form or 114 | documentation, if provided along with the Derivative Works; or, 115 | within a display generated by the Derivative Works, if and 116 | wherever such third-party notices normally appear. The contents 117 | of the NOTICE file are for informational purposes only and 118 | do not modify the License. You may add Your own attribution 119 | notices within Derivative Works that You distribute, alongside 120 | or as an addendum to the NOTICE text from the Work, provided 121 | that such additional attribution notices cannot be construed 122 | as modifying the License. 123 | 124 | You may add Your own copyright statement to Your modifications and 125 | may provide additional or different license terms and conditions 126 | for use, reproduction, or distribution of Your modifications, or 127 | for any such Derivative Works as a whole, provided Your use, 128 | reproduction, and distribution of the Work otherwise complies with 129 | the conditions stated in this License. 130 | 131 | 5. Submission of Contributions. Unless You explicitly state otherwise, 132 | any Contribution intentionally submitted for inclusion in the Work 133 | by You to the Licensor shall be under the terms and conditions of 134 | this License, without any additional terms or conditions. 135 | Notwithstanding the above, nothing herein shall supersede or modify 136 | the terms of any separate license agreement you may have executed 137 | with Licensor regarding such Contributions. 138 | 139 | 6. Trademarks. This License does not grant permission to use the trade 140 | names, trademarks, service marks, or product names of the Licensor, 141 | except as required for reasonable and customary use in describing the 142 | origin of the Work and reproducing the content of the NOTICE file. 143 | 144 | 7. Disclaimer of Warranty. Unless required by applicable law or 145 | agreed to in writing, Licensor provides the Work (and each 146 | Contributor provides its Contributions) on an "AS IS" BASIS, 147 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 148 | implied, including, without limitation, any warranties or conditions 149 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 150 | PARTICULAR PURPOSE. You are solely responsible for determining the 151 | appropriateness of using or redistributing the Work and assume any 152 | risks associated with Your exercise of permissions under this License. 153 | 154 | 8. Limitation of Liability. In no event and under no legal theory, 155 | whether in tort (including negligence), contract, or otherwise, 156 | unless required by applicable law (such as deliberate and grossly 157 | negligent acts) or agreed to in writing, shall any Contributor be 158 | liable to You for damages, including any direct, indirect, special, 159 | incidental, or consequential damages of any character arising as a 160 | result of this License or out of the use or inability to use the 161 | Work (including but not limited to damages for loss of goodwill, 162 | work stoppage, computer failure or malfunction, or any and all 163 | other commercial damages or losses), even if such Contributor 164 | has been advised of the possibility of such damages. 165 | 166 | 9. Accepting Warranty or Additional Liability. While redistributing 167 | the Work or Derivative Works thereof, You may choose to offer, 168 | and charge a fee for, acceptance of support, warranty, indemnity, 169 | or other liability obligations and/or rights consistent with this 170 | License. However, in accepting such obligations, You may act only 171 | on Your own behalf and on Your sole responsibility, not on behalf 172 | of any other Contributor, and only if You agree to indemnify, 173 | defend, and hold each Contributor harmless for any liability 174 | incurred by, or claims asserted against, such Contributor by reason 175 | of your accepting any such warranty or additional liability. 176 | 177 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 | # VirtualBMC for vSphere (vbmc4vsphere) 3 | 4 | [![Downloads](https://pepy.tech/badge/vbmc4vsphere)](https://pepy.tech/project/vbmc4vsphere) 5 | 6 | ⚠️ ***IMPORTANT UPDATES*** ⚠️ 7 | 8 | ***Since `0.1.0`, the commands have been renamed to `vsbmc` and `vsbmcd` to allow coexistence with the original VirtualBMC. Also, the path to the configuration files has been changed.*** 9 | 10 | ***To migrate your old configuration files, please refer to [the migration guide on the GitHub Wiki page](https://github.com/kurokobo/virtualbmc-for-vsphere/wiki/Migrate-configuration-files-from-0.0.8-or-earlier-to-0.1.0-or-later).*** 11 | 12 | 13 | ## Table of Contents 14 | 15 | - [Overview](#overview) 16 | - [Disclaimer](#disclaimer) 17 | - [Installation](#installation) 18 | - [vSphere Permissions](#vsphere-permissions) 19 | - [Supported IPMI commands](#supported-ipmi-commands) 20 | - [Architecture](#architecture) 21 | - [Quick Start](#quick-start) 22 | - [Installation](#installation-1) 23 | - [Start Daemon](#start-daemon) 24 | - [Configure VirtualBMC](#configure-virtualbmc) 25 | - [Server Simulation](#server-simulation) 26 | - [Tips](#tips) 27 | - [Optional configuration file](#optional-configuration-file) 28 | - [Manage stored data manually](#manage-stored-data-manually) 29 | - [Use in large-scale vSphere deployments](#use-in-large-scale-vsphere-deployments) 30 | - [Use with Nested-ESXi and vCenter Server](#use-with-nested-esxi-and-vcenter-server) 31 | - [Use with Nested-KVM and oVirt](#use-with-nested-kvm-and-ovirt) 32 | - [Use with OpenShift Bare Metal IPI](#use-with-openshift-bare-metal-ipi) 33 | - [Reference resources](#reference-resources) 34 | 35 | ## Overview 36 | 37 | A virtual BMC for controlling virtual machines using IPMI commands for the VMware vSphere environment. 38 | 39 | In other words, this is the VMware vSphere version of [VirtualBMC](https://github.com/openstack/virtualbmc) part of the OpenStack project. 40 | 41 | **This can be used as a BMC of Nested-ESXi**, therefore **you can make the vSphere DPM work in your nested environment** for testing purpose. 42 | 43 | ![Demo](https://user-images.githubusercontent.com/2920259/91665870-a7d78400-eb33-11ea-8d5b-33d98b3fe107.gif) 44 | 45 | See: 46 | 47 | - 📖[The guide to use with Nested-ESXi and vCenter Server](https://github.com/kurokobo/virtualbmc-for-vsphere/wiki/Use-with-Nested-ESXi-and-vCenter-Server). 48 | - 📖[The guide to use with Nested-KVM and oVirt](https://github.com/kurokobo/virtualbmc-for-vsphere/wiki/Use-with-Nested-KVM-and-oVirt). 49 | - 📖[The guide to use with OpenShift Bare Metal IPI](https://github.com/kurokobo/virtualbmc-for-vsphere/wiki/Install-OpenShift-in-vSphere-environment-using-the-Baremetal-IPI-procedure). 50 | 51 | ### Disclaimer 52 | 53 | - For testing purposes only. Not for production use. 54 | - The vCenter Server credentials including password are stored in plain text. 55 | - The vSphere DPM can be enabled with VirtualBMC for vSphere, but be careful with the recommendations presented in the vSphere DPM in nested environments may not be accurate or meet expectations. [See the wiki page for detail](https://github.com/kurokobo/virtualbmc-for-vsphere/wiki/Use-with-Nested-ESXi-and-vCenter-Server#notice). 56 | 57 | ### Installation 58 | 59 | ```bash 60 | python -m pip install vbmc4vsphere 61 | ``` 62 | 63 | If you want to run VirtualBMC for vSphere in Docker container, [see the guide on wiki page](https://github.com/kurokobo/virtualbmc-for-vsphere/wiki/Containerized-VirtualBMC-for-vSphere). 64 | 65 | ### vSphere Permissions 66 | 67 | The following are the minimum permissions needed on vSphere for VirtualBMC for vSphere (queried using [govc](https://github.com/vmware/govmomi/tree/master/govc)). 68 | 69 | ```text 70 | VirtualMachine.Config.Settings 71 | VirtualMachine.Interact.PowerOff 72 | VirtualMachine.Interact.PowerOn 73 | VirtualMachine.Interact.Reset 74 | Global.Diagnostics 75 | ``` 76 | 77 | ### Supported IPMI commands 78 | 79 | ```bash 80 | # Power the virtual machine on, off, graceful off, reset, and NMI. 81 | # Note that NMI is currently experimental. 82 | ipmitool -I lanplus -U admin -P password -H 192.168.0.1 -p 6230 power on|off|soft|reset|diag 83 | 84 | # Check the power status. 85 | ipmitool -I lanplus -U admin -P password -H 192.168.0.1 -p 6230 power status 86 | 87 | # Set the boot device to network, disk or cdrom. 88 | ipmitool -I lanplus -U admin -P password -H 192.168.0.1 -p 6230 chassis bootdev pxe|disk|cdrom 89 | 90 | # Get the current boot device. 91 | ipmitool -I lanplus -U admin -P password -H 192.168.0.1 -p 6230 chassis bootparam get 5 92 | 93 | # Get the channel info. Note that its output is always a dummy, not actual information. 94 | ipmitool -I lanplus -U admin -P password -H 192.168.0.1 -p 6230 channel info 95 | 96 | # Get the network info. Note that its output is always a dummy, not actual information. 97 | ipmitool -I lanplus -U admin -P password -H 192.168.0.1 -p 6230 lan print 1 98 | ``` 99 | 100 | - Experimental support: `power diag` 101 | - The command returns a response immediately, but the virtual machine receives NMI **60 seconds later**. This depends on the behavior of `debug-hung-vm` on the ESXi. 102 | 103 | ## Architecture 104 | 105 | ![Architecture](https://user-images.githubusercontent.com/2920259/91664084-c20b6500-eb27-11ea-8633-cc49ad6677d2.png) 106 | 107 | ## Quick Start 108 | 109 | Install VirtualBMC for vSphere on some linux host, start `vsbmcd` daemon, and then configure through `vsbmc` command. 110 | 111 | ### Installation 112 | 113 | ```bash 114 | python -m pip install vbmc4vsphere 115 | ``` 116 | 117 | ### Start Daemon 118 | 119 | - Start daemon: 120 | 121 | ```bash 122 | vsbmcd 123 | ``` 124 | 125 | By default, daemon starts in background. You can start it in foreground by `--foreground` option to get logs. 126 | 127 | ```bash 128 | vsbmcd --foreground 129 | ``` 130 | 131 | ### Configure VirtualBMC 132 | 133 | - In order to see all command options supported by the `vsbmc` tool do: 134 | 135 | ```bash 136 | vsbmc --help 137 | ``` 138 | 139 | It’s also possible to list the options from a specific command. For example, in order to know what can be provided as part of the `add` command do: 140 | 141 | ```bash 142 | vsbmc add --help 143 | ``` 144 | 145 | - Adding a new virtual BMC to control VM called lab-vesxi01: 146 | 147 | ```bash 148 | vsbmc add lab-vesxi01 --port 6230 --viserver 192.168.0.1 --viserver-username vsbmc@vsphere.local --viserver-password my-secure-password 149 | ``` 150 | 151 | - Binding a network port number below 1025 is restricted and only users with privilege will be able to start a virtual BMC on those ports. 152 | - Passing the credential for your vCenter Server is required. 153 | - By default, IPMI credential is configured as `admin` and `password`. You can specify your own username and password by `--username` and `--password` at this time. 154 | 155 | - Adding a additional virtual BMC to control VM called lab-vesxi02: 156 | 157 | ```bash 158 | vsbmc add lab-vesxi02 --port 6231 --viserver 192.168.0.1 --viserver-username vsbmc@vsphere.local --viserver-password my-secure-password 159 | ``` 160 | 161 | - Specify a different port for each virtual machine. 162 | - Starting the virtual BMC to control VMs: 163 | 164 | ```bash 165 | vsbmc start lab-vesxi01 166 | vsbmc start lab-vesxi02 167 | ``` 168 | 169 | - Getting the list of virtual BMCs including their VM name and IPMI network endpoints they are reachable at: 170 | 171 | ```bash 172 | $ vsbmc list 173 | +-------------+---------+---------+------+ 174 | | VM name | Status | Address | Port | 175 | +-------------+---------+---------+------+ 176 | | lab-vesxi01 | running | :: | 6230 | 177 | | lab-vesxi02 | running | :: | 6231 | 178 | +-------------+---------+---------+------+ 179 | ``` 180 | 181 | - To view configuration information for a specific virtual BMC: 182 | 183 | ```bash 184 | $ vsbmc show lab-vesxi01 185 | +-------------------+---------------------+ 186 | | Property | Value | 187 | +-------------------+---------------------+ 188 | | active | False | 189 | | address | :: | 190 | | password | *** | 191 | | port | 6230 | 192 | | status | running | 193 | | username | admin | 194 | | viserver | 192.168.0.1 | 195 | | viserver_password | *** | 196 | | viserver_username | vsbmc@vsphere.local | 197 | | vm_name | lab-vesxi01 | 198 | | vm_uuid | None | 199 | +-------------------+---------------------+ 200 | ``` 201 | 202 | - Stopping the virtual BMC: 203 | 204 | ```bash 205 | vsbmc stop lab-vesxi01 206 | vsbmc stop lab-vesxi02 207 | ``` 208 | 209 | ### Server Simulation 210 | 211 | Once the virtual BMC for a specific VM has been created and started you can then issue IPMI commands against the address and port of that virtual BMC to control the VM. 212 | 213 | In this example, if your VirtualBMC host has `192.168.0.100`, you can control: 214 | 215 | - `lab-vesxi01` through `192.168.0.100:6230` 216 | - `lab-vesxi02` through `192.168.0.100:6231` 217 | 218 | by using IPMI. For example: 219 | 220 | - To power on the virtual machine `lab-vesxi01`: 221 | 222 | ```bash 223 | $ ipmitool -I lanplus -H 192.168.0.100 -p 6230 -U admin -P password chassis power on 224 | Chassis Power Control: Up/On 225 | ``` 226 | 227 | - To check its power status: 228 | 229 | ```bash 230 | $ ipmitool -I lanplus -H 192.168.0.100 -p 6230 -U admin -P password chassis power status 231 | Chassis Power is on 232 | ``` 233 | 234 | - To shutdown `lab-vesxi01`: 235 | 236 | ```bash 237 | $ ipmitool -I lanplus -H 192.168.0.100 -p 6230 -U admin -P password chassis power soft 238 | Chassis Power Control: Soft 239 | ``` 240 | 241 | - To reset the `lab-vesxi02`: 242 | 243 | ```bash 244 | $ ipmitool -I lanplus -H 192.168.0.100 -p 6231 -U admin -P password chassis power reset 245 | Chassis Power Control: Reset 246 | ``` 247 | 248 | ## Tips 249 | 250 | ### Optional configuration file 251 | 252 | Both `vsbmcd` and `vsbmc` can make use of an optional configuration file, which is looked for in the following locations (in this order): 253 | 254 | - `VBMC4VSPHERE_CONFIG` environment variable pointing to a file 255 | - `$HOME/.vsbmc/vbmc4vsphere.conf` file 256 | - `/etc/vbmc4vsphere/vbmc4vsphere.conf` file 257 | 258 | If no configuration file has been found, the internal defaults apply. 259 | 260 | The configuration files are not created automatically unless you create them manually. And even if you don't create a configuration file, it won't matter in most cases. 261 | 262 | Below is a sample of `vbmc4vsphere.conf`. 263 | 264 | ```bash 265 | [default] 266 | #show_passwords = false 267 | config_dir = /home/vsbmc/.vsbmc 268 | #pid_file = /home/vsbmc/.vsbmc/master.pid 269 | #server_port = 50891 270 | #server_response_timeout = 5000 271 | #server_spawn_wait = 3000 272 | 273 | [log] 274 | # logfile = /home/vsbmc/.vsbmc/log/vbmc4vsphere.log 275 | debug = true 276 | 277 | [ipmi] 278 | session_timeout = 10 279 | ``` 280 | 281 | ### Manage stored data manually 282 | 283 | Once you invoke `vsbmc add` command, everything that you specified will be stored as `config` file per virtual machine under `$HOME/.vsbmc/` by default. There files can be used backup/restoration, migration, and of course can be managed by any kind of configuration management tools. Please note **everything including password stored in plain text** in these `config` file. 284 | 285 | The path for these files can be changed by `config_dir` in your `vbmc4vsphere.conf` described above. 286 | 287 | ```bash 288 | $ cat ~/.vsbmc/lab-vesxi01/config 289 | [VirtualBMC] 290 | username = admin 291 | password = password 292 | address = :: 293 | port = 6230 294 | vm_name = lab-vesxi01 295 | vm_uuid = 903a0dfb-68d1-4d2e-9674-10e353a733ca 296 | viserver = 192.168.0.1 297 | viserver_username = vsbmc@vsphere.local 298 | viserver_password = my-secure-password 299 | active = True 300 | ``` 301 | 302 | ### Use in large-scale vSphere deployments 303 | 304 | You can use UUID instead of name to identify virtual machine by specifying `--vm-uuid` option in `vsbmc add` command. This makes response time for IPMI command faster in large-scale vSphere deployments with a large number of virtual machines. 305 | 306 | ```bash 307 | vsbmc add lab-vesxi01 \ 308 | --vm-uuid 903a0dfb-68d1-4d2e-9674-10e353a733ca \ 309 | --port 6230 \ 310 | --viserver 192.168.0.1 \ 311 | --viserver-username vsbmc@vsphere.local \ 312 | --viserver-password my-secure-password 313 | ``` 314 | 315 | The UUID for virtual machines can be gathered in various ways like [govc](https://github.com/vmware/govmomi/tree/master/govc) and [PowerCLI](https://developer.vmware.com/powercli). 316 | 317 | ```bash 318 | # Get UUID by govc 319 | $ govc vm.info lab-vesxi01 320 | Name: lab-vesxi01 321 | ... 322 | UUID: 903a0dfb-68d1-4d2e-9674-10e353a733ca 323 | ... 324 | ``` 325 | 326 | ```powershell 327 | # Get UUID by PowerCLI 328 | > (Get-VM lab-vesxi01).ExtensionData.Config.Uuid 329 | 903a0dfb-68d1-4d2e-9674-10e353a733ca 330 | ``` 331 | 332 | ### Use with Nested-ESXi and vCenter Server 333 | 334 | In the vCenter Server, by using VirtualBMC for vSphere (`0.0.3` or later), **you can enable the vSphere DPM: Distributed Power Management feature** for Nested-ESXi host that is running in your VMware vSphere environment. 335 | 336 | So you can achieve: 337 | 338 | - Power-On the virtual ESXi in the same way as for physical ESXi. 339 | - Automated power on/off control of ESXi hosts based on the load of the host cluster by vCenter Server. 340 | 341 | See 📖[the guide on GitHub Wiki page to use with Nested-ESXi and vCenter Server](https://github.com/kurokobo/virtualbmc-for-vsphere/wiki/Use-with-Nested-ESXi-and-vCenter-Server). 342 | 343 | ### Use with Nested-KVM and oVirt 344 | 345 | In the oVirt, by using VirtualBMC for vSphere, you can enable the Power Management feature for Nested-KVM that is running in your vSphere environment. 346 | 347 | See 📖[the guide on GitHub Wiki page to use with Nested-KVM and oVirt](https://github.com/kurokobo/virtualbmc-for-vsphere/wiki/Use-with-Nested-KVM-and-oVirt). 348 | 349 | ### Use with OpenShift Bare Metal IPI 350 | 351 | With VirtualBMC for vSphere, you can control your virtual machines in the same way as a physical server. This means that tasks that require a physical BMC can be done in a virtual environment. 352 | 353 | One such example is the provisioning of a physical server. 354 | 355 | Here's how to automatically provision OpenShift to a physical server, called Bare Metal IPI, using a virtual machine in vSphere environment with VirtualBMC for vSphere. 356 | 357 | See 📖[the guide to GitHub Wiki page to use with OpenShift Bare Metal IPI](https://github.com/kurokobo/virtualbmc-for-vsphere/wiki/Install-OpenShift-in-vSphere-environment-using-the-Baremetal-IPI-procedure). 358 | 359 | ## Reference resources 360 | 361 | This project is started based on the copy of [VirtualBMC 2.1.0.dev](https://github.com/openstack/virtualbmc/commit/c4c8edb66bc49fcb1b8fb41af77546e06d2e8bce) and customized to support the VMware vSphere environment instead of the OpenStack. 362 | 363 | - Original VirtualBMC documentation (for OpenStack): 364 | - Its source: 365 | -------------------------------------------------------------------------------- /docker/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM python:3.10-slim 2 | ARG UID=1000 3 | 4 | WORKDIR /vsbmc 5 | RUN useradd -d /vsbmc -u ${UID} vsbmc && \ 6 | chown -R vsbmc:root /vsbmc && \ 7 | chmod -R 770 /vsbmc 8 | 9 | RUN python -m pip install vbmc4vsphere 10 | 11 | USER vsbmc 12 | 13 | ENTRYPOINT ["vsbmcd"] 14 | CMD ["--foreground"] 15 | -------------------------------------------------------------------------------- /docker/README.md: -------------------------------------------------------------------------------- 1 | # Containerized VirtualBMC for vSphere 2 | 3 | If you want to run VirtualBMC for vSphere in Docker container, [see the guide on wiki page](https://github.com/kurokobo/virtualbmc-for-vsphere/wiki/Containerized-VirtualBMC-for-vSphere). 4 | -------------------------------------------------------------------------------- /docker/docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: "3" 2 | 3 | services: 4 | vbmc4vsphere: 5 | container_name: vbmc4vsphere 6 | image: ghcr.io/kurokobo/vbmc4vsphere:0.3.0 7 | networks: 8 | - vsbmc-network 9 | ports: 10 | - "6231:6231/udp" 11 | - "6232:6232/udp" 12 | - "6233:6233/udp" 13 | # - "192.168.0.242:623:6231/udp" 14 | # - "192.168.0.243:623:6232/udp" 15 | # - "192.168.0.244:623:6233/udp" 16 | volumes: 17 | - vsbmc-volume:/vsbmc 18 | 19 | volumes: 20 | vsbmc-volume: 21 | 22 | networks: 23 | vsbmc-network: 24 | driver: "bridge" 25 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | # The order of packages is significant, because pip processes them in the order 2 | # of appearance. Changing the order has an impact on the overall integration 3 | # process, which may cause wedges in the gate later. 4 | 5 | pbr!=2.1.0,>=2.0.0 # Apache-2.0 6 | pyghmi==1.5.16 # Apache-2.0 7 | cliff!=2.9.0,>=2.8.0 # Apache-2.0 8 | pyzmq>=14.3.1 # LGPL+BSD 9 | pyvmomi>=7.0 # Apache-2.0 10 | -------------------------------------------------------------------------------- /setup.cfg: -------------------------------------------------------------------------------- 1 | [metadata] 2 | name = vbmc4vsphere 3 | summary = Create virtual BMCs for controlling virtual instances via IPMI for vSphere environment 4 | long_description = file: README.md 5 | long_description_content_type = text/markdown 6 | author = kurokobo 7 | author-email = 2920259+kurokobo@users.noreply.github.com 8 | home-page = https://github.com/kurokobo/virtualbmc-for-vsphere 9 | python-requires = >=3.6 10 | classifier = 11 | Environment :: Other Environment 12 | Intended Audience :: Information Technology 13 | Intended Audience :: System Administrators 14 | License :: OSI Approved :: Apache Software License 15 | Operating System :: POSIX :: Linux 16 | Programming Language :: Python 17 | Programming Language :: Python :: Implementation :: CPython 18 | Programming Language :: Python :: 3 :: Only 19 | Programming Language :: Python :: 3 20 | Programming Language :: Python :: 3.6 21 | Programming Language :: Python :: 3.7 22 | Programming Language :: Python :: 3.8 23 | Programming Language :: Python :: 3.9 24 | Programming Language :: Python :: 3.10 25 | 26 | [files] 27 | packages = 28 | vbmc4vsphere 29 | 30 | [entry_points] 31 | console_scripts = 32 | vsbmc = vbmc4vsphere.cmd.vsbmc:main 33 | vsbmcd = vbmc4vsphere.cmd.vsbmcd:main 34 | 35 | vbmc4vsphere = 36 | add = vbmc4vsphere.cmd.vsbmc:AddCommand 37 | delete = vbmc4vsphere.cmd.vsbmc:DeleteCommand 38 | start = vbmc4vsphere.cmd.vsbmc:StartCommand 39 | stop = vbmc4vsphere.cmd.vsbmc:StopCommand 40 | list = vbmc4vsphere.cmd.vsbmc:ListCommand 41 | show = vbmc4vsphere.cmd.vsbmc:ShowCommand 42 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2013 Hewlett-Packard Development Company, L.P. 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 12 | # implied. 13 | # See the License for the specific language governing permissions and 14 | # limitations under the License. 15 | 16 | import setuptools 17 | 18 | setuptools.setup(setup_requires=["pbr>=2.0.0"], pbr=True) 19 | -------------------------------------------------------------------------------- /vbmc4vsphere/__init__.py: -------------------------------------------------------------------------------- 1 | # Licensed under the Apache License, Version 2.0 (the "License"); you may 2 | # not use this file except in compliance with the License. You may obtain 3 | # a copy of the License at 4 | # 5 | # http://www.apache.org/licenses/LICENSE-2.0 6 | # 7 | # Unless required by applicable law or agreed to in writing, software 8 | # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 9 | # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 10 | # License for the specific language governing permissions and limitations 11 | # under the License. 12 | 13 | import pbr.version 14 | 15 | __version__ = pbr.version.VersionInfo("vbmc4vsphere").version_string() 16 | -------------------------------------------------------------------------------- /vbmc4vsphere/cmd/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kurokobo/virtualbmc-for-vsphere/ab2a5ddeac7d974cbe120c2018c70d5bf8ae2ef2/vbmc4vsphere/cmd/__init__.py -------------------------------------------------------------------------------- /vbmc4vsphere/cmd/vsbmc.py: -------------------------------------------------------------------------------- 1 | # Licensed under the Apache License, Version 2.0 (the "License"); you may 2 | # not use this file except in compliance with the License. You may obtain 3 | # a copy of the License at 4 | # 5 | # http://www.apache.org/licenses/LICENSE-2.0 6 | # 7 | # Unless required by applicable law or agreed to in writing, software 8 | # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 9 | # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 10 | # License for the specific language governing permissions and limitations 11 | # under the License. 12 | 13 | import json 14 | import logging 15 | import sys 16 | 17 | import zmq 18 | from cliff.app import App 19 | from cliff.command import Command 20 | from cliff.commandmanager import CommandManager 21 | from cliff.lister import Lister 22 | 23 | import vbmc4vsphere 24 | from vbmc4vsphere import config as vbmc_config 25 | from vbmc4vsphere import log 26 | from vbmc4vsphere.exception import VirtualBMCError 27 | 28 | CONF = vbmc_config.get_config() 29 | 30 | LOG = log.get_logger() 31 | 32 | 33 | class ZmqClient(object): 34 | """Client part of the VirtualBMC system. 35 | 36 | The command-line client tool communicates with the server part 37 | of the VirtualBMC system by exchanging JSON-encoded messages. 38 | 39 | Client builds requests out of its command-line options which 40 | include the command (e.g. `start`, `list` etc) and command-specific 41 | options. 42 | 43 | Server response is a JSON document which contains at least the 44 | `rc` and `msg` attributes, used to indicate the outcome of the 45 | command, and optionally 2-D table conveyed through the `header` 46 | and `rows` attributes pointing to lists of cell values. 47 | """ 48 | 49 | SERVER_TIMEOUT = CONF["default"]["server_response_timeout"] 50 | 51 | @staticmethod 52 | def to_dict(obj): 53 | return { 54 | attr: getattr(obj, attr) for attr in dir(obj) if not attr.startswith("_") 55 | } 56 | 57 | def communicate(self, command, args, no_daemon=False): 58 | 59 | data_out = self.to_dict(args) 60 | 61 | data_out.update(command=command) 62 | 63 | data_out = json.dumps(data_out) 64 | 65 | server_port = CONF["default"]["server_port"] 66 | 67 | context = socket = None 68 | 69 | try: 70 | context = zmq.Context() 71 | socket = context.socket(zmq.REQ) 72 | socket.setsockopt(zmq.LINGER, 5) 73 | socket.connect("tcp://127.0.0.1:%s" % server_port) 74 | 75 | poller = zmq.Poller() 76 | poller.register(socket, zmq.POLLIN) 77 | 78 | try: 79 | socket.send(data_out.encode("utf-8")) 80 | 81 | socks = dict(poller.poll(timeout=self.SERVER_TIMEOUT)) 82 | if socket in socks and socks[socket] == zmq.POLLIN: 83 | data_in = socket.recv() 84 | 85 | else: 86 | raise zmq.ZMQError(zmq.RCVTIMEO, msg="Server response timed out") 87 | 88 | except zmq.ZMQError as ex: 89 | msg = ( 90 | "Failed to connect to the vsbmcd server on port " 91 | "%(port)s, error: %(error)s" % {"port": server_port, "error": ex} 92 | ) 93 | LOG.error(msg) 94 | raise VirtualBMCError(msg) 95 | 96 | finally: 97 | if socket: 98 | socket.close() 99 | context.destroy() 100 | 101 | try: 102 | data_in = json.loads(data_in.decode("utf-8")) 103 | 104 | except ValueError as ex: 105 | msg = "Server response parsing error %(error)s" % {"error": ex} 106 | LOG.error(msg) 107 | raise VirtualBMCError(msg) 108 | 109 | rc = data_in.pop("rc", None) 110 | if rc: 111 | msg = "(%(rc)s): %(msg)s" % { 112 | "rc": rc, 113 | "msg": "\n".join(data_in.get("msg", ())), 114 | } 115 | LOG.error(msg) 116 | raise VirtualBMCError(msg) 117 | 118 | return data_in 119 | 120 | 121 | class AddCommand(Command): 122 | """Create a new BMC for a virtual machine instance""" 123 | 124 | def get_parser(self, prog_name): 125 | parser = super(AddCommand, self).get_parser(prog_name) 126 | 127 | parser.add_argument("vm_name", help="The name of the virtual machine") 128 | parser.add_argument( 129 | "--vm-uuid", 130 | dest="vm_uuid", 131 | default=None, 132 | help=( 133 | "The UUID of the virtual machine. " 134 | "If this specified, UUID is used instead of name " 135 | "to identify virtual machine; defaults to None" 136 | ), 137 | ) 138 | parser.add_argument( 139 | "--username", 140 | dest="username", 141 | default="admin", 142 | help='The BMC username; defaults to "admin"', 143 | ) 144 | parser.add_argument( 145 | "--password", 146 | dest="password", 147 | default="password", 148 | help='The BMC password; defaults to "password"', 149 | ) 150 | parser.add_argument( 151 | "--port", 152 | dest="port", 153 | type=int, 154 | default=6230, 155 | help="Port to listen on; defaults to 6230", 156 | ) 157 | parser.add_argument( 158 | "--address", 159 | dest="address", 160 | default="::", 161 | help=( 162 | "The address to bind to (IPv4 and IPv6 " 163 | 'are supported); defaults to "::"' 164 | ), 165 | ) 166 | parser.add_argument( 167 | "--fakemac", 168 | dest="fakemac", 169 | default=None, 170 | help=( 171 | "The fake MAC address to use to sanity check " 172 | "by vCenter Server. Used only when register " 173 | "as BMC for ESXi; defaults to ganarete from " 174 | '"vm_name" automatically' 175 | ), 176 | ) 177 | parser.add_argument( 178 | "--viserver", 179 | dest="viserver", 180 | default=None, 181 | help="The VI Server; defaults to None", 182 | ) 183 | parser.add_argument( 184 | "--viserver-username", 185 | dest="viserver_username", 186 | default=None, 187 | help="The VI Server username; defaults to None", 188 | ) 189 | parser.add_argument( 190 | "--viserver-password", 191 | dest="viserver_password", 192 | default=None, 193 | help="The VI Server password; defaults to None", 194 | ) 195 | return parser 196 | 197 | def take_action(self, args): 198 | 199 | log = logging.getLogger(__name__) 200 | 201 | # Check if the username and password were given for VI Server 202 | viuser = args.viserver_username 203 | vipass = args.viserver_password 204 | if any((viuser, vipass)): 205 | if not all((viuser, vipass)): 206 | msg = ( 207 | "A password and username are required to use " 208 | "VI Server authentication" 209 | ) 210 | log.error(msg) 211 | raise VirtualBMCError(msg) 212 | 213 | self.app.zmq.communicate("add", args, no_daemon=self.app.options.no_daemon) 214 | 215 | 216 | class DeleteCommand(Command): 217 | """Delete a virtual BMC for a virtual machine instance""" 218 | 219 | def get_parser(self, prog_name): 220 | parser = super(DeleteCommand, self).get_parser(prog_name) 221 | 222 | parser.add_argument( 223 | "vm_names", nargs="+", help="A list of virtual machine names" 224 | ) 225 | 226 | return parser 227 | 228 | def take_action(self, args): 229 | self.app.zmq.communicate("delete", args, self.app.options.no_daemon) 230 | 231 | 232 | class StartCommand(Command): 233 | """Start a virtual BMC for a virtual machine instance""" 234 | 235 | def get_parser(self, prog_name): 236 | parser = super(StartCommand, self).get_parser(prog_name) 237 | 238 | parser.add_argument( 239 | "vm_names", nargs="+", help="A list of virtual machine names" 240 | ) 241 | 242 | return parser 243 | 244 | def take_action(self, args): 245 | self.app.zmq.communicate("start", args, no_daemon=self.app.options.no_daemon) 246 | 247 | 248 | class StopCommand(Command): 249 | """Stop a virtual BMC for a virtual machine instance""" 250 | 251 | def get_parser(self, prog_name): 252 | parser = super(StopCommand, self).get_parser(prog_name) 253 | 254 | parser.add_argument( 255 | "vm_names", nargs="+", help="A list of virtual machine names" 256 | ) 257 | 258 | return parser 259 | 260 | def take_action(self, args): 261 | self.app.zmq.communicate("stop", args, no_daemon=self.app.options.no_daemon) 262 | 263 | 264 | class ListCommand(Lister): 265 | """List all virtual BMC instances""" 266 | 267 | def get_parser(self, prog_name): 268 | parser = super(ListCommand, self).get_parser(prog_name) 269 | 270 | parser.add_argument( 271 | "--fakemac", action="store_true", help="Display Fake MAC column" 272 | ) 273 | 274 | return parser 275 | 276 | def take_action(self, args): 277 | rsp = self.app.zmq.communicate( 278 | "list", args, no_daemon=self.app.options.no_daemon 279 | ) 280 | return rsp["header"], sorted(rsp["rows"]) 281 | 282 | 283 | class ShowCommand(Lister): 284 | """Show virtual BMC properties""" 285 | 286 | def get_parser(self, prog_name): 287 | parser = super(ShowCommand, self).get_parser(prog_name) 288 | 289 | parser.add_argument("vm_name", help="The name of the virtual machine") 290 | 291 | return parser 292 | 293 | def take_action(self, args): 294 | rsp = self.app.zmq.communicate( 295 | "show", args, no_daemon=self.app.options.no_daemon 296 | ) 297 | return rsp["header"], sorted(rsp["rows"]) 298 | 299 | 300 | class VirtualBMCApp(App): 301 | def __init__(self): 302 | super(VirtualBMCApp, self).__init__( 303 | description="Virtual Baseboard Management Controller (BMC) backed " 304 | "by virtual machines", 305 | version=vbmc4vsphere.__version__, 306 | command_manager=CommandManager("vbmc4vsphere"), 307 | deferred_help=True, 308 | ) 309 | 310 | def build_option_parser(self, description, version, argparse_kwargs=None): 311 | parser = super(VirtualBMCApp, self).build_option_parser( 312 | description, version, argparse_kwargs 313 | ) 314 | 315 | parser.add_argument( 316 | "--no-daemon", 317 | action="store_true", 318 | help="Do not start vsbmcd automatically", 319 | ) 320 | 321 | return parser 322 | 323 | def initialize_app(self, argv): 324 | self.zmq = ZmqClient() 325 | 326 | def clean_up(self, cmd, result, err): 327 | self.LOG.debug("clean_up %(name)s", {"name": cmd.__class__.__name__}) 328 | if err: 329 | self.LOG.debug("got an error: %(error)s", {"error": err}) 330 | 331 | 332 | def main(argv=sys.argv[1:]): 333 | vbmc_app = VirtualBMCApp() 334 | return vbmc_app.run(argv) 335 | 336 | 337 | if __name__ == "__main__": 338 | sys.exit(main()) 339 | -------------------------------------------------------------------------------- /vbmc4vsphere/cmd/vsbmcd.py: -------------------------------------------------------------------------------- 1 | # Licensed under the Apache License, Version 2.0 (the "License"); you may 2 | # not use this file except in compliance with the License. You may obtain 3 | # a copy of the License at 4 | # 5 | # http://www.apache.org/licenses/LICENSE-2.0 6 | # 7 | # Unless required by applicable law or agreed to in writing, software 8 | # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 9 | # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 10 | # License for the specific language governing permissions and limitations 11 | # under the License. 12 | 13 | import argparse 14 | import os 15 | import sys 16 | import tempfile 17 | 18 | import vbmc4vsphere 19 | from vbmc4vsphere import config as vbmc_config 20 | from vbmc4vsphere import control, log, utils 21 | 22 | LOG = log.get_logger() 23 | 24 | CONF = vbmc_config.get_config() 25 | 26 | 27 | def main(argv=sys.argv[1:]): 28 | parser = argparse.ArgumentParser( 29 | prog="VirtualBMC server", 30 | description="A virtual BMC server for controlling virtual instances", 31 | ) 32 | parser.add_argument("--version", action="version", version=vbmc4vsphere.__version__) 33 | parser.add_argument( 34 | "--foreground", action="store_true", default=False, help="Do not daemonize" 35 | ) 36 | 37 | args = parser.parse_args(argv) 38 | 39 | pid_file = CONF["default"]["pid_file"] 40 | 41 | try: 42 | with open(pid_file) as f: 43 | pid = int(f.read()) 44 | 45 | os.kill(pid, 0) 46 | 47 | except Exception: 48 | pass 49 | 50 | else: 51 | LOG.error("server PID #%(pid)d still running", {"pid": pid}) 52 | return 1 53 | 54 | def wrap_with_pidfile(func, pid): 55 | dir_name = os.path.dirname(pid_file) 56 | 57 | if not os.path.exists(dir_name): 58 | os.makedirs(dir_name, mode=0o700) 59 | 60 | try: 61 | with tempfile.NamedTemporaryFile( 62 | mode="w+t", dir=dir_name, delete=False 63 | ) as f: 64 | f.write(str(pid)) 65 | os.rename(f.name, pid_file) 66 | 67 | func() 68 | 69 | except Exception as e: 70 | LOG.error("%(error)s", {"error": e}) 71 | return 1 72 | 73 | finally: 74 | try: 75 | os.unlink(pid_file) 76 | 77 | except Exception: 78 | pass 79 | 80 | if args.foreground: 81 | return wrap_with_pidfile(control.application, os.getpid()) 82 | 83 | else: 84 | with utils.detach_process() as pid: 85 | if pid > 0: 86 | return 0 87 | 88 | return wrap_with_pidfile(control.application, pid) 89 | 90 | 91 | if __name__ == "__main__": 92 | sys.exit(main()) 93 | -------------------------------------------------------------------------------- /vbmc4vsphere/config.py: -------------------------------------------------------------------------------- 1 | # Licensed under the Apache License, Version 2.0 (the "License"); you may 2 | # not use this file except in compliance with the License. You may obtain 3 | # a copy of the License at 4 | # 5 | # http://www.apache.org/licenses/LICENSE-2.0 6 | # 7 | # Unless required by applicable law or agreed to in writing, software 8 | # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 9 | # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 10 | # License for the specific language governing permissions and limitations 11 | # under the License. 12 | 13 | import configparser 14 | import os 15 | 16 | from vbmc4vsphere import utils 17 | 18 | __all__ = ["get_config"] 19 | 20 | _CONFIG_FILE_PATHS = ( 21 | os.environ.get("VBMC4VSPHERE_CONFIG", ""), 22 | os.path.join(os.path.expanduser("~"), ".vsbmc", "vbmc4vsphere.conf"), 23 | "/etc/vbmc4vsphere/vbmc4vsphere.conf", 24 | ) 25 | 26 | CONFIG_FILE = next((x for x in _CONFIG_FILE_PATHS if os.path.exists(x)), "") 27 | 28 | CONFIG = None 29 | 30 | 31 | class VirtualBMCConfig(object): 32 | 33 | DEFAULTS = { 34 | "default": { 35 | "show_passwords": "false", 36 | "config_dir": os.path.join(os.path.expanduser("~"), ".vsbmc"), 37 | "pid_file": os.path.join(os.path.expanduser("~"), ".vsbmc", "master.pid"), 38 | "server_port": 50891, 39 | "server_response_timeout": 5000, # milliseconds 40 | "server_spawn_wait": 3000, # milliseconds 41 | }, 42 | "log": {"logfile": None, "debug": "true"}, 43 | "ipmi": { 44 | # Maximum time (in seconds) to wait for the data to come across 45 | "session_timeout": 1 46 | }, 47 | } 48 | 49 | def initialize(self): 50 | config = configparser.ConfigParser() 51 | config.read(CONFIG_FILE) 52 | self._conf_dict = self._as_dict(config) 53 | self._validate() 54 | 55 | def _as_dict(self, config): 56 | conf_dict = self.DEFAULTS 57 | for section in config.sections(): 58 | if section not in conf_dict: 59 | conf_dict[section] = {} 60 | for key, val in config.items(section): 61 | conf_dict[section][key] = val 62 | 63 | return conf_dict 64 | 65 | def _validate(self): 66 | self._conf_dict["log"]["debug"] = utils.str2bool( 67 | self._conf_dict["log"]["debug"] 68 | ) 69 | 70 | self._conf_dict["default"]["show_passwords"] = utils.str2bool( 71 | self._conf_dict["default"]["show_passwords"] 72 | ) 73 | 74 | self._conf_dict["default"]["server_port"] = int( 75 | self._conf_dict["default"]["server_port"] 76 | ) 77 | 78 | self._conf_dict["default"]["server_spawn_wait"] = int( 79 | self._conf_dict["default"]["server_spawn_wait"] 80 | ) 81 | 82 | self._conf_dict["default"]["server_response_timeout"] = int( 83 | self._conf_dict["default"]["server_response_timeout"] 84 | ) 85 | 86 | self._conf_dict["ipmi"]["session_timeout"] = int( 87 | self._conf_dict["ipmi"]["session_timeout"] 88 | ) 89 | 90 | def __getitem__(self, key): 91 | return self._conf_dict[key] 92 | 93 | 94 | def get_config(): 95 | global CONFIG 96 | if CONFIG is None: 97 | CONFIG = VirtualBMCConfig() 98 | CONFIG.initialize() 99 | 100 | return CONFIG 101 | -------------------------------------------------------------------------------- /vbmc4vsphere/control.py: -------------------------------------------------------------------------------- 1 | # Copyright 2017 Red Hat, Inc. 2 | # All Rights Reserved. 3 | # 4 | # Licensed under the Apache License, Version 2.0 (the "License"); you may 5 | # not use this file except in compliance with the License. You may obtain 6 | # 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, WITHOUT 12 | # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 13 | # License for the specific language governing permissions and limitations 14 | # under the License. 15 | 16 | import json 17 | import signal 18 | import sys 19 | 20 | import zmq 21 | 22 | from vbmc4vsphere import config as vbmc_config 23 | from vbmc4vsphere import exception, log 24 | from vbmc4vsphere.manager import VirtualBMCManager 25 | 26 | CONF = vbmc_config.get_config() 27 | 28 | LOG = log.get_logger() 29 | 30 | TIMER_PERIOD = 3000 # milliseconds 31 | 32 | 33 | def main_loop(vbmc_manager, handle_command): 34 | """Server part of the CLI control interface 35 | 36 | Receives JSON messages from ZMQ socket, calls the command handler and 37 | sends JSON response back to the client. 38 | 39 | Client builds requests out of its command-line options which 40 | include the command (e.g. `start`, `list` etc) and command-specific 41 | options. 42 | 43 | Server handles the commands and responds with a JSON document which 44 | contains at least the `rc` and `msg` attributes, used to indicate the 45 | outcome of the command, and optionally 2-D table conveyed through the 46 | `header` and `rows` attributes pointing to lists of cell values. 47 | """ 48 | server_port = CONF["default"]["server_port"] 49 | 50 | context = socket = None 51 | 52 | try: 53 | context = zmq.Context() 54 | socket = context.socket(zmq.REP) 55 | socket.setsockopt(zmq.LINGER, 5) 56 | socket.bind("tcp://127.0.0.1:%s" % server_port) 57 | 58 | poller = zmq.Poller() 59 | poller.register(socket, zmq.POLLIN) 60 | 61 | LOG.info("Started vBMC server on port %s", server_port) 62 | 63 | while True: 64 | socks = dict(poller.poll(timeout=TIMER_PERIOD)) 65 | if socket in socks and socks[socket] == zmq.POLLIN: 66 | message = socket.recv() 67 | else: 68 | vbmc_manager.periodic() 69 | continue 70 | 71 | try: 72 | data_in = json.loads(message.decode("utf-8")) 73 | 74 | except ValueError as ex: 75 | LOG.warning( 76 | "Control server request deserialization error: " "%(error)s", 77 | {"error": ex}, 78 | ) 79 | continue 80 | 81 | LOG.debug("Command request data: %(request)s", {"request": data_in}) 82 | 83 | try: 84 | data_out = handle_command(vbmc_manager, data_in) 85 | 86 | except exception.VirtualBMCError as ex: 87 | msg = "Command failed: %(error)s" % {"error": ex} 88 | LOG.error(msg) 89 | data_out = {"rc": 1, "msg": [msg]} 90 | 91 | LOG.debug("Command response data: %(response)s", {"response": data_out}) 92 | 93 | try: 94 | message = json.dumps(data_out) 95 | 96 | except ValueError as ex: 97 | LOG.warning( 98 | "Control server response serialization error: " "%(error)s", 99 | {"error": ex}, 100 | ) 101 | continue 102 | 103 | socket.send(message.encode("utf-8")) 104 | 105 | finally: 106 | if socket: 107 | socket.close() 108 | if context: 109 | context.destroy() 110 | 111 | 112 | def command_dispatcher(vbmc_manager, data_in): 113 | """Control CLI command dispatcher 114 | 115 | Calls vBMC manager to execute commands, implements uniform 116 | dictionary-based interface to the caller. 117 | """ 118 | command = data_in.pop("command") 119 | 120 | LOG.debug('Running "%(cmd)s" command handler', {"cmd": command}) 121 | 122 | if command == "add": 123 | 124 | # Check if the username and password were given for VI Server 125 | vi_user = data_in["viserver_username"] 126 | vi_pass = data_in["viserver_password"] 127 | if any((vi_user, vi_pass)): 128 | if not all((vi_user, vi_pass)): 129 | error = ( 130 | "A password and username are required to use " 131 | "VI Server authentication" 132 | ) 133 | return {"msg": [error], "rc": 1} 134 | 135 | rc, msg = vbmc_manager.add(**data_in) 136 | 137 | return {"rc": rc, "msg": [msg] if msg else []} 138 | 139 | elif command == "delete": 140 | data_out = [ 141 | vbmc_manager.delete(vm_name) for vm_name in set(data_in["vm_names"]) 142 | ] 143 | return { 144 | "rc": max(rc for rc, msg in data_out), 145 | "msg": [msg for rc, msg in data_out if msg], 146 | } 147 | 148 | elif command == "start": 149 | data_out = [vbmc_manager.start(vm_name) for vm_name in set(data_in["vm_names"])] 150 | return { 151 | "rc": max(rc for rc, msg in data_out), 152 | "msg": [msg for rc, msg in data_out if msg], 153 | } 154 | 155 | elif command == "stop": 156 | data_out = [vbmc_manager.stop(vm_name) for vm_name in set(data_in["vm_names"])] 157 | return { 158 | "rc": max(rc for rc, msg in data_out), 159 | "msg": [msg for rc, msg in data_out if msg], 160 | } 161 | 162 | elif command == "list": 163 | rc, tables = vbmc_manager.list() 164 | 165 | if data_in["fakemac"]: 166 | header = ("VM name", "Status", "Address", "Port", "Fake MAC") 167 | keys = ("vm_name", "status", "address", "port", "fakemac") 168 | else: 169 | header = ("VM name", "Status", "Address", "Port") 170 | keys = ("vm_name", "status", "address", "port") 171 | 172 | return { 173 | "rc": rc, 174 | "header": header, 175 | "rows": [[table.get(key, "?") for key in keys] for table in tables], 176 | } 177 | 178 | elif command == "show": 179 | rc, table = vbmc_manager.show(data_in["vm_name"]) 180 | 181 | return { 182 | "rc": rc, 183 | "header": ("Property", "Value"), 184 | "rows": table, 185 | } 186 | 187 | else: 188 | return { 189 | "rc": 1, 190 | "msg": ["Unknown command"], 191 | } 192 | 193 | 194 | def application(): 195 | """vsbmcd application entry point 196 | 197 | Initializes, serves and cleans up everything. 198 | """ 199 | vbmc_manager = VirtualBMCManager() 200 | 201 | vbmc_manager.periodic() 202 | 203 | def kill_children(*args): 204 | vbmc_manager.periodic(shutdown=True) 205 | sys.exit(0) 206 | 207 | # SIGTERM does not seem to propagate to multiprocessing 208 | signal.signal(signal.SIGTERM, kill_children) 209 | 210 | try: 211 | main_loop(vbmc_manager, command_dispatcher) 212 | except KeyboardInterrupt: 213 | LOG.info("Got keyboard interrupt, exiting") 214 | vbmc_manager.periodic(shutdown=True) 215 | except Exception as ex: 216 | LOG.error("Control server error: %(error)s", {"error": ex}) 217 | vbmc_manager.periodic(shutdown=True) 218 | -------------------------------------------------------------------------------- /vbmc4vsphere/exception.py: -------------------------------------------------------------------------------- 1 | # Licensed under the Apache License, Version 2.0 (the "License"); you may 2 | # not use this file except in compliance with the License. You may obtain 3 | # a copy of the License at 4 | # 5 | # http://www.apache.org/licenses/LICENSE-2.0 6 | # 7 | # Unless required by applicable law or agreed to in writing, software 8 | # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 9 | # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 10 | # License for the specific language governing permissions and limitations 11 | # under the License. 12 | 13 | 14 | class VirtualBMCError(Exception): 15 | message = None 16 | 17 | def __init__(self, message=None, **kwargs): 18 | if self.message and kwargs: 19 | self.message = self.message % kwargs 20 | else: 21 | self.message = message 22 | 23 | super(VirtualBMCError, self).__init__(self.message) 24 | 25 | 26 | class VMAlreadyExists(VirtualBMCError): 27 | message = "VM %(vm)s already exists" 28 | 29 | 30 | class VMNotFound(VirtualBMCError): 31 | message = "No VM with matching name %(vm)s was found" 32 | 33 | 34 | class VMNotFoundByUUID(VirtualBMCError): 35 | message = "No VM with matching UUID %(uuid)s was found" 36 | 37 | 38 | class VIServerConnectionOpenError(VirtualBMCError): 39 | message = ( 40 | 'Fail to establish a connection with VI Server "%(vi)s". ' "Error: %(error)s" 41 | ) 42 | 43 | 44 | class DetachProcessError(VirtualBMCError): 45 | message = ( 46 | "Error when forking (detaching) the VirtualBMC process " 47 | "from its parent and session. Error: %(error)s" 48 | ) 49 | -------------------------------------------------------------------------------- /vbmc4vsphere/log.py: -------------------------------------------------------------------------------- 1 | # Licensed under the Apache License, Version 2.0 (the "License"); you may 2 | # not use this file except in compliance with the License. You may obtain 3 | # a copy of the License at 4 | # 5 | # http://www.apache.org/licenses/LICENSE-2.0 6 | # 7 | # Unless required by applicable law or agreed to in writing, software 8 | # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 9 | # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 10 | # License for the specific language governing permissions and limitations 11 | # under the License. 12 | 13 | import errno 14 | import logging 15 | 16 | from vbmc4vsphere import config 17 | 18 | __all__ = ["get_logger"] 19 | 20 | DEFAULT_LOG_FORMAT = ( 21 | "%(asctime)s.%(msecs)03d %(process)d %(levelname)s " "%(name)s [-] %(message)s" 22 | ) 23 | LOGGER = None 24 | 25 | 26 | class VirtualBMCLogger(logging.Logger): 27 | def __init__(self, debug=False, logfile=None): 28 | logging.Logger.__init__(self, "VirtualBMC") 29 | try: 30 | if logfile is not None: 31 | self.handler = logging.FileHandler(logfile) 32 | else: 33 | self.handler = logging.StreamHandler() 34 | 35 | formatter = logging.Formatter(DEFAULT_LOG_FORMAT) 36 | self.handler.setFormatter(formatter) 37 | self.addHandler(self.handler) 38 | 39 | if debug: 40 | self.setLevel(logging.DEBUG) 41 | else: 42 | self.setLevel(logging.INFO) 43 | 44 | except IOError as e: 45 | if e.errno == errno.EACCES: 46 | pass 47 | 48 | 49 | def get_logger(): 50 | global LOGGER 51 | if LOGGER is None: 52 | log_conf = config.get_config()["log"] 53 | LOGGER = VirtualBMCLogger(debug=log_conf["debug"], logfile=log_conf["logfile"]) 54 | 55 | return LOGGER 56 | -------------------------------------------------------------------------------- /vbmc4vsphere/manager.py: -------------------------------------------------------------------------------- 1 | # Licensed under the Apache License, Version 2.0 (the "License"); you may 2 | # not use this file except in compliance with the License. You may obtain 3 | # a copy of the License at 4 | # 5 | # http://www.apache.org/licenses/LICENSE-2.0 6 | # 7 | # Unless required by applicable law or agreed to in writing, software 8 | # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 9 | # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 10 | # License for the specific language governing permissions and limitations 11 | # under the License. 12 | 13 | import configparser 14 | import errno 15 | import multiprocessing 16 | import os 17 | import shutil 18 | import signal 19 | 20 | from vbmc4vsphere import config as vbmc_config 21 | from vbmc4vsphere import exception, log, utils 22 | from vbmc4vsphere.vbmc import VirtualBMC 23 | 24 | LOG = log.get_logger() 25 | 26 | # BMC status 27 | RUNNING = "running" 28 | DOWN = "down" 29 | ERROR = "error" 30 | 31 | DEFAULT_SECTION = "VirtualBMC" 32 | 33 | CONF = vbmc_config.get_config() 34 | 35 | # Always default to 'fork' multiprocessing 36 | multiprocessing.set_start_method("fork") 37 | 38 | 39 | class VirtualBMCManager(object): 40 | 41 | VBMC_OPTIONS = [ 42 | "username", 43 | "password", 44 | "address", 45 | "port", 46 | "fakemac", 47 | "vm_name", 48 | "vm_uuid", 49 | "viserver", 50 | "viserver_username", 51 | "viserver_password", 52 | "active", 53 | ] 54 | 55 | def __init__(self): 56 | super(VirtualBMCManager, self).__init__() 57 | self.config_dir = CONF["default"]["config_dir"] 58 | self._running_vms = {} 59 | 60 | def _parse_config(self, vm_name): 61 | config_path = os.path.join(self.config_dir, vm_name, "config") 62 | if not os.path.exists(config_path): 63 | raise exception.VMNotFound(vm=vm_name) 64 | 65 | try: 66 | config = configparser.ConfigParser() 67 | config.read(config_path) 68 | 69 | bmc = {} 70 | for item in self.VBMC_OPTIONS: 71 | try: 72 | value = config.get(DEFAULT_SECTION, item) 73 | except configparser.NoOptionError: 74 | value = None 75 | 76 | bmc[item] = value 77 | 78 | # Generate Fake MAC if needed 79 | if bmc["fakemac"] is None: 80 | bmc["fakemac"] = utils.generate_fakemac_by_vm_name(vm_name) 81 | 82 | # Port needs to be int 83 | bmc["port"] = config.getint(DEFAULT_SECTION, "port") 84 | 85 | return bmc 86 | 87 | except OSError: 88 | raise exception.VMNotFound(vm=vm_name) 89 | 90 | def _store_config(self, **options): 91 | config = configparser.ConfigParser() 92 | config.add_section(DEFAULT_SECTION) 93 | 94 | for option, value in options.items(): 95 | if value is not None: 96 | config.set(DEFAULT_SECTION, option, str(value)) 97 | 98 | config_path = os.path.join(self.config_dir, options["vm_name"], "config") 99 | 100 | with open(config_path, "w") as f: 101 | config.write(f) 102 | 103 | def _vbmc_enabled(self, vm_name, lets_enable=None, config=None): 104 | if not config: 105 | config = self._parse_config(vm_name) 106 | 107 | try: 108 | currently_enabled = utils.str2bool(config["active"]) 109 | 110 | except Exception: 111 | currently_enabled = False 112 | 113 | if lets_enable is not None and lets_enable != currently_enabled: 114 | config.update(active=lets_enable) 115 | self._store_config(**config) 116 | currently_enabled = lets_enable 117 | 118 | return currently_enabled 119 | 120 | def _sync_vbmc_states(self, shutdown=False): 121 | """Starts/stops vBMC instances 122 | 123 | Walks over vBMC instances configuration, starts 124 | enabled but dead instances, kills non-configured 125 | but alive ones. 126 | """ 127 | 128 | def vbmc_runner(bmc_config): 129 | # The manager process installs a signal handler for SIGTERM to 130 | # propagate it to children. Return to the default handler. 131 | signal.signal(signal.SIGTERM, signal.SIG_DFL) 132 | 133 | show_passwords = CONF["default"]["show_passwords"] 134 | 135 | if show_passwords: 136 | show_options = bmc_config 137 | else: 138 | show_options = utils.mask_dict_password(bmc_config) 139 | 140 | try: 141 | vbmc = VirtualBMC(**bmc_config) 142 | 143 | except Exception as ex: 144 | LOG.exception( 145 | "Error running vBMC with configuration " "%(opts)s: %(error)s", 146 | {"opts": show_options, "error": ex}, 147 | ) 148 | return 149 | 150 | try: 151 | vbmc.listen(timeout=CONF["ipmi"]["session_timeout"]) 152 | 153 | except Exception as ex: 154 | LOG.exception( 155 | "Shutdown vBMC for vm %(vm)s, cause " "%(error)s", 156 | {"vm": show_options["vm_name"], "error": ex}, 157 | ) 158 | return 159 | 160 | for vm_name in os.listdir(self.config_dir): 161 | if not os.path.isdir(os.path.join(self.config_dir, vm_name)): 162 | continue 163 | 164 | try: 165 | bmc_config = self._parse_config(vm_name) 166 | 167 | except exception.VMNotFound: 168 | continue 169 | 170 | if shutdown: 171 | lets_enable = False 172 | else: 173 | lets_enable = self._vbmc_enabled(vm_name, config=bmc_config) 174 | 175 | instance = self._running_vms.get(vm_name) 176 | 177 | if lets_enable: 178 | 179 | if not instance or not instance.is_alive(): 180 | 181 | instance = multiprocessing.Process( 182 | name="vbmcd-managing-vm-%s" % vm_name, 183 | target=vbmc_runner, 184 | args=(bmc_config,), 185 | ) 186 | 187 | instance.daemon = True 188 | instance.start() 189 | 190 | self._running_vms[vm_name] = instance 191 | 192 | LOG.info( 193 | "Started vBMC instance for vm " "%(vm)s", 194 | {"vm": vm_name}, 195 | ) 196 | 197 | if not instance.is_alive(): 198 | LOG.debug( 199 | "Found dead vBMC instance for vm %(vm)s " "(rc %(rc)s)", 200 | {"vm": vm_name, "rc": instance.exitcode}, 201 | ) 202 | 203 | else: 204 | if instance: 205 | if instance.is_alive(): 206 | instance.terminate() 207 | LOG.info( 208 | "Terminated vBMC instance for vm " "%(vm)s", 209 | {"vm": vm_name}, 210 | ) 211 | 212 | self._running_vms.pop(vm_name, None) 213 | 214 | def _show(self, vm_name): 215 | bmc_config = self._parse_config(vm_name) 216 | 217 | show_passwords = CONF["default"]["show_passwords"] 218 | 219 | if show_passwords: 220 | show_options = bmc_config 221 | else: 222 | show_options = utils.mask_dict_password(bmc_config) 223 | 224 | instance = self._running_vms.get(vm_name) 225 | 226 | if instance and instance.is_alive(): 227 | show_options["status"] = RUNNING 228 | elif instance and not instance.is_alive(): 229 | show_options["status"] = ERROR 230 | else: 231 | show_options["status"] = DOWN 232 | 233 | return show_options 234 | 235 | def periodic(self, shutdown=False): 236 | self._sync_vbmc_states(shutdown) 237 | 238 | def add( 239 | self, 240 | username, 241 | password, 242 | port, 243 | address, 244 | fakemac, 245 | vm_name, 246 | vm_uuid, 247 | viserver, 248 | viserver_username, 249 | viserver_password, 250 | **kwargs 251 | ): 252 | 253 | vm_path = os.path.join(self.config_dir, vm_name) 254 | 255 | try: 256 | os.makedirs(vm_path) 257 | except OSError as ex: 258 | if ex.errno == errno.EEXIST: 259 | return 1, str(ex) 260 | 261 | msg = "Failed to create vm %(vm)s. " "Error: %(error)s" % { 262 | "vm": vm_name, 263 | "error": ex, 264 | } 265 | LOG.error(msg) 266 | return 1, msg 267 | 268 | if fakemac is None: 269 | fakemac = utils.generate_fakemac_by_vm_name(vm_name) 270 | 271 | try: 272 | self._store_config( 273 | vm_name=vm_name, 274 | vm_uuid=vm_uuid, 275 | username=username, 276 | password=password, 277 | port=str(port), 278 | address=address, 279 | fakemac=fakemac.replace("-", ":"), 280 | viserver=viserver, 281 | viserver_username=viserver_username, 282 | viserver_password=viserver_password, 283 | active=False, 284 | ) 285 | 286 | except Exception as ex: 287 | self.delete(vm_name) 288 | return 1, str(ex) 289 | 290 | return 0, "" 291 | 292 | def delete(self, vm_name): 293 | vm_path = os.path.join(self.config_dir, vm_name) 294 | if not os.path.exists(vm_path): 295 | raise exception.VMNotFound(vm=vm_name) 296 | 297 | try: 298 | self.stop(vm_name) 299 | except exception.VirtualBMCError: 300 | pass 301 | 302 | shutil.rmtree(vm_path) 303 | 304 | return 0, "" 305 | 306 | def start(self, vm_name): 307 | try: 308 | bmc_config = self._parse_config(vm_name) 309 | 310 | except Exception as ex: 311 | return 1, str(ex) 312 | 313 | if vm_name in self._running_vms: 314 | 315 | self._sync_vbmc_states() 316 | 317 | if vm_name in self._running_vms: 318 | LOG.warning( 319 | "BMC instance %(vm)s already running, ignoring " 320 | '"start" command' % {"vm": vm_name} 321 | ) 322 | return 0, "" 323 | 324 | try: 325 | self._vbmc_enabled(vm_name, config=bmc_config, lets_enable=True) 326 | 327 | except Exception as e: 328 | LOG.exception("Failed to start vm %s", vm_name) 329 | return ( 330 | 1, 331 | ( 332 | "Failed to start vm %(vm)s. Error: " 333 | "%(error)s" % {"vm": vm_name, "error": e} 334 | ), 335 | ) 336 | 337 | self._sync_vbmc_states() 338 | 339 | return 0, "" 340 | 341 | def stop(self, vm_name): 342 | try: 343 | self._vbmc_enabled(vm_name, lets_enable=False) 344 | 345 | except Exception as ex: 346 | LOG.exception("Failed to stop vm %s", vm_name) 347 | return 1, str(ex) 348 | 349 | self._sync_vbmc_states() 350 | 351 | return 0, "" 352 | 353 | def list(self): 354 | rc = 0 355 | tables = [] 356 | try: 357 | for vm in os.listdir(self.config_dir): 358 | if os.path.isdir(os.path.join(self.config_dir, vm)): 359 | tables.append(self._show(vm)) 360 | 361 | except OSError as e: 362 | if e.errno == errno.EEXIST: 363 | rc = 1 364 | 365 | return rc, tables 366 | 367 | def show(self, vm_name): 368 | return 0, list(self._show(vm_name).items()) 369 | -------------------------------------------------------------------------------- /vbmc4vsphere/utils.py: -------------------------------------------------------------------------------- 1 | # Licensed under the Apache License, Version 2.0 (the "License"); you may 2 | # not use this file except in compliance with the License. You may obtain 3 | # a copy of the License at 4 | # 5 | # http://www.apache.org/licenses/LICENSE-2.0 6 | # 7 | # Unless required by applicable law or agreed to in writing, software 8 | # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 9 | # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 10 | # License for the specific language governing permissions and limitations 11 | # under the License. 12 | 13 | import hashlib 14 | import os 15 | import re 16 | import ssl 17 | import sys 18 | import urllib.parse 19 | import urllib.request 20 | from threading import Thread 21 | 22 | from pyVim.connect import Disconnect, SmartConnect 23 | from pyVmomi import vim 24 | 25 | from vbmc4vsphere import exception 26 | 27 | 28 | class viserver_open(object): 29 | def __init__(self, vi, vi_username=None, vi_password=None, readonly=False): 30 | self.vi = vi 31 | self.vi_username = vi_username 32 | self.vi_password = vi_password 33 | self.readonly = readonly 34 | 35 | def __enter__(self): 36 | context = None 37 | if hasattr(ssl, "_create_unverified_context"): 38 | context = ssl._create_unverified_context() 39 | try: 40 | self.conn = SmartConnect( 41 | host=self.vi, 42 | user=self.vi_username, 43 | pwd=self.vi_password, 44 | # port=self.vi_port, 45 | sslContext=context, 46 | ) 47 | if not self.conn: 48 | raise Exception 49 | except Exception as e: 50 | raise exception.VIServerConnectionOpenError(vi=self.vi, error=e) 51 | 52 | return self.conn 53 | 54 | def __exit__(self, type, value, traceback): 55 | _ = Disconnect(self.conn) 56 | 57 | 58 | def get_obj_by_name(conn, root, vim_type, value): 59 | objs = [] 60 | container = conn.content.viewManager.CreateContainerView(root, vim_type, True) 61 | for obj in container.view: 62 | if obj.name == value: 63 | objs.append(obj) 64 | container.Destroy() 65 | return objs 66 | 67 | 68 | def get_viserver_vm_by_uuid(conn, uuid): 69 | try: 70 | search_index = conn.content.searchIndex 71 | vm = search_index.FindByUuid(None, uuid, True) 72 | 73 | if vm: 74 | return vm 75 | else: 76 | raise Exception 77 | 78 | except Exception: 79 | raise exception.VMNotFoundByUUID(uuid=uuid) 80 | 81 | 82 | def get_viserver_vm(conn, vm): 83 | try: 84 | vms = get_obj_by_name(conn, conn.content.rootFolder, [vim.VirtualMachine], vm) 85 | 86 | if len(vms) != 1: 87 | raise Exception 88 | 89 | return vms[0] 90 | 91 | except Exception: 92 | raise exception.VMNotFound(vm=vm) 93 | 94 | 95 | def get_bootable_device_type(conn, boot_dev): 96 | if isinstance(boot_dev, vim.vm.BootOptions.BootableFloppyDevice): 97 | return "floppy" 98 | elif isinstance(boot_dev, vim.vm.BootOptions.BootableDiskDevice): 99 | return "disk" 100 | elif isinstance(boot_dev, vim.vm.BootOptions.BootableCdromDevice): 101 | return "cdrom" 102 | elif isinstance(boot_dev, vim.vm.BootOptions.BootableEthernetDevice): 103 | return "ethernet" 104 | 105 | 106 | def set_boot_device(conn, vm, device): 107 | """Set boot device to specified device. 108 | 109 | https://github.com/ansible-collections/vmware/blob/main/plugins/module_utils/vmware.py 110 | """ 111 | 112 | boot_order_list = [] 113 | if device == "cdrom": 114 | bootable_cdroms = [ 115 | dev 116 | for dev in vm.config.hardware.device 117 | if isinstance(dev, vim.vm.device.VirtualCdrom) 118 | ] 119 | if bootable_cdroms: 120 | boot_order_list.append(vim.vm.BootOptions.BootableCdromDevice()) 121 | elif device == "disk": 122 | bootable_disks = [ 123 | dev 124 | for dev in vm.config.hardware.device 125 | if isinstance(dev, vim.vm.device.VirtualDisk) 126 | ] 127 | if bootable_disks: 128 | boot_order_list.extend( 129 | [ 130 | vim.vm.BootOptions.BootableDiskDevice(deviceKey=bootable_disk.key) 131 | for bootable_disk in bootable_disks 132 | ] 133 | ) 134 | elif device == "ethernet": 135 | bootable_ethernets = [ 136 | dev 137 | for dev in vm.config.hardware.device 138 | if isinstance(dev, vim.vm.device.VirtualEthernetCard) 139 | ] 140 | if bootable_ethernets: 141 | boot_order_list.extend( 142 | [ 143 | vim.vm.BootOptions.BootableEthernetDevice( 144 | deviceKey=bootable_ethernet.key 145 | ) 146 | for bootable_ethernet in bootable_ethernets 147 | ] 148 | ) 149 | elif device == "floppy": 150 | bootable_floppy = [ 151 | dev 152 | for dev in vm.config.hardware.device 153 | if isinstance(dev, vim.vm.device.VirtualFloppy) 154 | ] 155 | if bootable_floppy: 156 | boot_order_list.append(vim.vm.BootOptions.BootableFloppyDevice()) 157 | 158 | kwargs = dict() 159 | kwargs.update({"bootOrder": boot_order_list}) 160 | 161 | vm_conf = vim.vm.ConfigSpec() 162 | vm_conf.bootOptions = vim.vm.BootOptions(**kwargs) 163 | vm.ReconfigVM_Task(vm_conf) 164 | return 165 | 166 | 167 | def send_nmi(conn, vm): 168 | """Send NMI to specified VM. 169 | 170 | https://github.com/vmware/pyvmomi/issues/726 171 | """ 172 | context = None 173 | if hasattr(ssl, "_create_unverified_context"): 174 | context = ssl._create_unverified_context() 175 | 176 | vmx_path = vm.config.files.vmPathName 177 | for ds_url in vm.config.datastoreUrl: 178 | vmx_path = vmx_path.replace("[%s] " % ds_url.name, "%s/" % ds_url.url) 179 | 180 | url = "https://%s/cgi-bin/vm-support.cgi?manifests=%s&vm=%s" % ( 181 | vm.runtime.host.name, 182 | urllib.parse.quote_plus("HungVM:Send_NMI_To_Guest"), 183 | urllib.parse.quote_plus(vmx_path), 184 | ) 185 | 186 | spec = vim.SessionManager.HttpServiceRequestSpec(method="httpGet", url=url) 187 | ticket = conn.content.sessionManager.AcquireGenericServiceTicket(spec) 188 | headers = { 189 | "Cookie": "vmware_cgi_ticket=%s" % ticket.id, 190 | } 191 | 192 | req = urllib.request.Request(url, headers=headers) 193 | Thread( 194 | target=urllib.request.urlopen, args=(req,), kwargs={"context": context} 195 | ).start() 196 | 197 | 198 | def is_pid_running(pid): 199 | try: 200 | os.kill(pid, 0) 201 | return True 202 | except OSError: 203 | return False 204 | 205 | 206 | def str2bool(string): 207 | lower = string.lower() 208 | if lower not in ("true", "false"): 209 | raise ValueError('Value "%s" can not be interpreted as ' "boolean" % string) 210 | return lower == "true" 211 | 212 | 213 | def mask_dict_password(dictionary, secret="***"): 214 | """Replace passwords with a secret in a dictionary.""" 215 | d = dictionary.copy() 216 | for k in d: 217 | if "password" in k: 218 | d[k] = secret 219 | return d 220 | 221 | 222 | def generate_fakemac_by_vm_name(vm_name): 223 | hash = hashlib.md5(vm_name.encode()).digest() 224 | fakemac = ":".join( 225 | "%02x" % b for b in [0x02, 0x00, 0x00, hash[0], hash[1], hash[2]] 226 | ) 227 | return fakemac 228 | 229 | 230 | def convert_fakemac_string_to_bytes(fakemac_str): 231 | fakemac_bytes = [int(b, 16) for b in re.split(":|-", fakemac_str)] 232 | return fakemac_bytes 233 | 234 | 235 | class detach_process(object): 236 | """Detach the process from its parent and session.""" 237 | 238 | def _fork(self, parent_exits): 239 | try: 240 | pid = os.fork() 241 | if pid > 0 and parent_exits: 242 | os._exit(0) 243 | 244 | return pid 245 | 246 | except OSError as e: 247 | raise exception.DetachProcessError(error=e) 248 | 249 | def _change_root_directory(self): 250 | """Change to root directory. 251 | 252 | Ensure that our process doesn't keep any directory in use. Failure 253 | to do this could make it so that an administrator couldn't 254 | unmount a filesystem, because it was our current directory. 255 | """ 256 | try: 257 | os.chdir("/") 258 | except Exception as e: 259 | error = "Failed to change root directory. Error: %s" % e 260 | raise exception.DetachProcessError(error=error) 261 | 262 | def _change_file_creation_mask(self): 263 | """Set the umask for new files. 264 | 265 | Set the umask for new files the process creates so that it does 266 | have complete control over the permissions of them. We don't 267 | know what umask we may have inherited. 268 | """ 269 | try: 270 | os.umask(0) 271 | except Exception as e: 272 | error = "Failed to change file creation mask. Error: %s" % e 273 | raise exception.DetachProcessError(error=error) 274 | 275 | def __enter__(self): 276 | pid = self._fork(parent_exits=False) 277 | if pid > 0: 278 | return pid 279 | 280 | os.setsid() 281 | 282 | self._fork(parent_exits=True) 283 | 284 | self._change_root_directory() 285 | self._change_file_creation_mask() 286 | 287 | sys.stdout.flush() 288 | sys.stderr.flush() 289 | 290 | si = open(os.devnull, "r") 291 | so = open(os.devnull, "a+") 292 | se = open(os.devnull, "a+") 293 | 294 | os.dup2(si.fileno(), sys.stdin.fileno()) 295 | os.dup2(so.fileno(), sys.stdout.fileno()) 296 | os.dup2(se.fileno(), sys.stderr.fileno()) 297 | 298 | return pid 299 | 300 | def __exit__(self, type, value, traceback): 301 | pass 302 | -------------------------------------------------------------------------------- /vbmc4vsphere/vbmc.py: -------------------------------------------------------------------------------- 1 | # Licensed under the Apache License, Version 2.0 (the "License"); you may 2 | # not use this file except in compliance with the License. You may obtain 3 | # a copy of the License at 4 | # 5 | # http://www.apache.org/licenses/LICENSE-2.0 6 | # 7 | # Unless required by applicable law or agreed to in writing, software 8 | # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 9 | # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 10 | # License for the specific language governing permissions and limitations 11 | # under the License. 12 | 13 | # import xml.etree.ElementTree as ET 14 | 15 | import struct 16 | import traceback 17 | 18 | import pyghmi.ipmi.bmc as bmc 19 | import pyghmi.ipmi.private.session as ipmisession 20 | from pyghmi.ipmi.private.serversession import IpmiServer as ipmiserver 21 | from pyghmi.ipmi.private.serversession import ServerSession as serversession 22 | 23 | from vbmc4vsphere import exception, log, utils 24 | 25 | LOG = log.get_logger() 26 | 27 | # Power states 28 | POWEROFF = 0 29 | POWERON = 1 30 | 31 | # From the IPMI - Intelligent Platform Management Interface Specification 32 | # Second Generation v2.0 Document Revision 1.1 October 1, 2013 33 | # https://www.intel.com/content/dam/www/public/us/en/documents/product-briefs/ipmi-second-gen-interface-spec-v2-rev1-1.pdf 34 | # 35 | # Command failed and can be retried 36 | IPMI_COMMAND_NODE_BUSY = 0xC0 37 | # Invalid data field in request 38 | IPMI_INVALID_DATA = 0xCC 39 | 40 | 41 | # Boot device maps 42 | GET_BOOT_DEVICES_MAP = { 43 | "ethernet": 0x4, 44 | "disk": 0x8, 45 | "cdrom": 0x14, 46 | "floppy": 0x3C, 47 | } 48 | 49 | SET_BOOT_DEVICES_MAP = { 50 | "network": "ethernet", 51 | "hd": "disk", 52 | "optical": "cdrom", 53 | "floppy": "floppy", 54 | } 55 | 56 | 57 | def _get_vm_object(conn, vm_obj): 58 | """Simple wrapper to chose lookup method""" 59 | if vm_obj.vm_uuid: 60 | LOG.debug('UUID lookup method called for vm uuid %s' % vm_obj.vm_uuid) 61 | return utils.get_viserver_vm_by_uuid(conn, vm_obj.vm_uuid) 62 | return utils.get_viserver_vm(conn, vm_obj.vm_name) 63 | 64 | 65 | def sessionless_data(self, data, sockaddr): 66 | """Examines unsolocited packet and decides appropriate action. 67 | 68 | For a listening IpmiServer, a packet without an active session 69 | comes here for examination. If it is something that is utterly 70 | sessionless (e.g. get channel authentication), send the appropriate 71 | response. If it is a get session challenge or open rmcp+ request, 72 | spawn a session to handle the context. 73 | 74 | Patched by VirtualBMC for vSphere to handle sessionless IPMIv2 75 | packet and ASF Presence Ping. 76 | Based on pyghmi 1.5.16, Apache License 2.0 77 | https://opendev.org/x/pyghmi/src/branch/master/pyghmi/ipmi/private/serversession.py 78 | """ 79 | data = bytearray(data) 80 | if len(data) < 22: 81 | if data[0:4] == b"\x06\x00\xff\x06" and data[8] == 0x80: # asf presence ping 82 | LOG.info("Responding to asf presence ping") 83 | self.send_asf_presence_pong(data, sockaddr) 84 | else: 85 | return 86 | if not (data[0] == 6 and data[2:4] == b"\xff\x07"): # not ipmi 87 | return 88 | authtype = data[4] 89 | if authtype == 6: # ipmi 2 payload... 90 | payloadtype = data[5] 91 | if payloadtype not in (0, 16): 92 | return 93 | if payloadtype == 16: # new session to handle conversation 94 | serversession( 95 | self.authdata, 96 | self.kg, 97 | sockaddr, 98 | self.serversocket, 99 | data[16:], 100 | self.uuid, 101 | bmc=self, 102 | ) 103 | return 104 | # ditch two byte, because ipmi2 header is two 105 | # bytes longer than ipmi1 (payload type added, payload length 2). 106 | data = data[2:] 107 | myaddr, netfnlun = struct.unpack("2B", bytes(data[14:16])) 108 | netfn = (netfnlun & 0b11111100) >> 2 109 | mylun = netfnlun & 0b11 110 | if netfn == 6: # application request 111 | if data[19] == 0x38: # cmd = get channel auth capabilities 112 | verchannel, level = struct.unpack("2B", bytes(data[20:22])) 113 | version = verchannel & 0b10000000 114 | if version != 0b10000000: 115 | return 116 | channel = verchannel & 0b1111 117 | if channel != 0xE: 118 | return 119 | (clientaddr, clientlun) = struct.unpack("BB", bytes(data[17:19])) 120 | clientseq = clientlun >> 2 121 | clientlun &= 0b11 # Lun is only the least significant bits 122 | level &= 0b1111 123 | if authtype == 6: 124 | self.send_auth_cap_v2( 125 | myaddr, mylun, clientaddr, clientlun, clientseq, sockaddr 126 | ) 127 | else: 128 | self.send_auth_cap( 129 | myaddr, mylun, clientaddr, clientlun, clientseq, sockaddr 130 | ) 131 | elif data[19] == 0x54: 132 | clientaddr, clientlun = data[17:19] 133 | clientseq = clientlun >> 2 134 | clientlun &= 0b11 135 | self.send_cipher_suites( 136 | myaddr, mylun, clientaddr, clientlun, clientseq, data, sockaddr 137 | ) 138 | 139 | 140 | def send_auth_cap_v2(self, myaddr, mylun, clientaddr, clientlun, clientseq, sockaddr): 141 | """Send response to "get channel auth cap (0x38)" command with IPMI 2.0 headers. 142 | 143 | Copied from send_auth_cap function and modified to send response 144 | in the form of IPMI 2.0. 145 | Based on pyghmi 1.5.16, Apache License 2.0 146 | https://opendev.org/x/pyghmi/src/branch/master/pyghmi/ipmi/private/serversession.py 147 | """ 148 | header = bytearray( 149 | b"\x06\x00\xff\x07\x06\x00\x00\x00\x00\x00\x00\x00\x00\x00\x10\x00" 150 | ) 151 | headerdata = [clientaddr, clientlun | (7 << 2)] 152 | headersum = ipmisession._checksum(*headerdata) 153 | header += bytearray( 154 | headerdata + [headersum, myaddr, mylun | (clientseq << 2), 0x38] 155 | ) 156 | header += self.authcap 157 | bodydata = struct.unpack("B" * len(header[19:]), bytes(header[19:])) 158 | 159 | header.append(ipmisession._checksum(*bodydata)) 160 | ipmisession._io_sendto(self.serversocket, header, sockaddr) 161 | 162 | 163 | def send_asf_presence_pong(self, data, sockaddr): 164 | """Send response to ASF Presence Ping.""" 165 | header = bytearray( 166 | b"\x06\x00\xff\x06\x00\x00\x11\xbe\x40" 167 | + struct.pack("B", data[9]) 168 | + b"\x00\x10\x00\x00\x11\xbe\x00\x00\x00\x00\x81\x00\x00\x00\x00\x00\x00\x00" 169 | ) 170 | ipmisession._io_sendto(self.serversocket, header, sockaddr) 171 | 172 | 173 | # Patch pyghmi with modified functions 174 | ipmiserver.sessionless_data = sessionless_data 175 | ipmiserver.send_auth_cap_v2 = send_auth_cap_v2 176 | ipmiserver.send_asf_presence_pong = send_asf_presence_pong 177 | 178 | 179 | class VirtualBMC(bmc.Bmc): 180 | def __init__( 181 | self, 182 | username, 183 | password, 184 | port, 185 | address, 186 | fakemac, 187 | vm_name, 188 | vm_uuid, 189 | viserver, 190 | viserver_username=None, 191 | viserver_password=None, 192 | **kwargs 193 | ): 194 | super(VirtualBMC, self).__init__( 195 | {username: password}, port=port, address=address 196 | ) 197 | self.vm_name = vm_name 198 | self.vm_uuid = vm_uuid 199 | self.fakemac = fakemac 200 | self._conn_args = { 201 | "vi": viserver, 202 | "vi_username": viserver_username, 203 | "vi_password": viserver_password, 204 | } 205 | 206 | def get_boot_device(self): 207 | LOG.debug("Get boot device called for %(vm)s", {"vm": self.vm_name}) 208 | 209 | try: 210 | with utils.viserver_open(**self._conn_args) as conn: 211 | vm = _get_vm_object(conn, self) 212 | boot_element = vm.config.bootOptions.bootOrder 213 | boot_dev = None 214 | if boot_element: 215 | boot_dev = utils.get_bootable_device_type(conn, boot_element[0]) 216 | LOG.debug("Boot device is: %s" % boot_dev) 217 | return GET_BOOT_DEVICES_MAP.get(boot_dev, 0) 218 | return IPMI_COMMAND_NODE_BUSY 219 | except Exception as e: 220 | msg = "Error getting boot device of vm %(vm)s. " "Error: %(error)s" % { 221 | "vm": self.vm_name, 222 | "error": e, 223 | } 224 | LOG.error(msg) 225 | raise exception.VirtualBMCError(message=msg) 226 | 227 | def _remove_boot_elements(self, parent_element): 228 | for boot_element in parent_element.findall("boot"): 229 | parent_element.remove(boot_element) 230 | 231 | def set_boot_device(self, bootdevice): 232 | LOG.debug( 233 | "Set boot device called for %(vm)s with boot " 'device "%(bootdev)s"', 234 | {"vm": self.vm_name, "bootdev": bootdevice}, 235 | ) 236 | device = SET_BOOT_DEVICES_MAP.get(bootdevice) 237 | if device is None: 238 | # Invalid data field in request 239 | return IPMI_INVALID_DATA 240 | try: 241 | with utils.viserver_open(**self._conn_args) as conn: 242 | vm = _get_vm_object(conn, self) 243 | utils.set_boot_device(conn, vm, device) 244 | except Exception as e: 245 | LOG.error( 246 | "Failed setting the boot device %(bootdev)s for vm %(vm)s." 247 | "Error: %(error)s", 248 | {"bootdev": device, "vm": self.vm_name, "error": e}, 249 | ) 250 | # Command failed, but let client to retry 251 | return IPMI_COMMAND_NODE_BUSY 252 | 253 | def get_power_state(self): 254 | LOG.debug("Get power state called for vm %(vm)s", {"vm": self.vm_name}) 255 | 256 | try: 257 | with utils.viserver_open(**self._conn_args) as conn: 258 | vm = _get_vm_object(conn, self) 259 | if "poweredOn" == vm.runtime.powerState: 260 | return POWERON 261 | except Exception as e: 262 | msg = "Error getting the power state of vm %(vm)s. " "Error: %(error)s" % { 263 | "vm": self.vm_name, 264 | "error": e, 265 | } 266 | LOG.error(msg) 267 | raise exception.VirtualBMCError(message=msg) 268 | 269 | return POWEROFF 270 | 271 | def pulse_diag(self): 272 | LOG.debug("Power diag called for vm %(vm)s", {"vm": self.vm_name}) 273 | try: 274 | with utils.viserver_open(**self._conn_args) as conn: 275 | vm = _get_vm_object(conn, self) 276 | utils.send_nmi(conn, vm) 277 | LOG.debug( 278 | "The NMI will be sent to the vm %(vm)s 60 seconds later", 279 | {"vm": self.vm_name}, 280 | ) 281 | except Exception as e: 282 | LOG.error( 283 | "Error powering diag the vm %(vm)s. " "Error: %(error)s", 284 | {"vm": self.vm_name, "error": e}, 285 | ) 286 | # Command failed, but let client to retry 287 | return IPMI_COMMAND_NODE_BUSY 288 | 289 | def power_off(self): 290 | LOG.debug("Power off called for vm %(vm)s", {"vm": self.vm_name}) 291 | try: 292 | with utils.viserver_open(**self._conn_args) as conn: 293 | vm = _get_vm_object(conn, self) 294 | if "poweredOn" == vm.runtime.powerState: 295 | vm.PowerOff() 296 | except Exception as e: 297 | LOG.error( 298 | "Error powering off the vm %(vm)s. " "Error: %(error)s", 299 | {"vm": self.vm_name, "error": e}, 300 | ) 301 | # Command failed, but let client to retry 302 | return IPMI_COMMAND_NODE_BUSY 303 | 304 | def power_on(self): 305 | LOG.debug("Power on called for vm %(vm)s", {"vm": self.vm_name}) 306 | try: 307 | with utils.viserver_open(**self._conn_args) as conn: 308 | vm = _get_vm_object(conn, self) 309 | if "poweredOn" != vm.runtime.powerState: 310 | vm.PowerOn() 311 | except Exception as e: 312 | LOG.error( 313 | "Error powering on the vm %(vm)s. " "Error: %(error)s", 314 | {"vm": self.vm_name, "error": e}, 315 | ) 316 | # Command failed, but let client to retry 317 | return IPMI_COMMAND_NODE_BUSY 318 | 319 | def power_shutdown(self): 320 | LOG.debug("Soft power off called for vm %(vm)s", {"vm": self.vm_name}) 321 | try: 322 | with utils.viserver_open(**self._conn_args) as conn: 323 | vm = _get_vm_object(conn, self) 324 | if "poweredOn" == vm.runtime.powerState: 325 | vm.ShutdownGuest() 326 | except Exception as e: 327 | LOG.error( 328 | "Error soft powering off the vm %(vm)s. " "Error: %(error)s", 329 | {"vm": self.vm_name, "error": e}, 330 | ) 331 | # Command failed, but let client to retry 332 | return IPMI_COMMAND_NODE_BUSY 333 | 334 | def power_reset(self): 335 | LOG.debug("Power reset called for vm %(vm)s", {"vm": self.vm_name}) 336 | try: 337 | with utils.viserver_open(**self._conn_args) as conn: 338 | vm = _get_vm_object(conn, self) 339 | if "poweredOn" == vm.runtime.powerState: 340 | vm.Reset() 341 | except Exception as e: 342 | LOG.error( 343 | "Error reseting the vm %(vm)s. " "Error: %(error)s", 344 | {"vm": self.vm_name, "error": e}, 345 | ) 346 | # Command not supported in present state 347 | return IPMI_COMMAND_NODE_BUSY 348 | 349 | def get_channel_access(self, request, session): 350 | """Fake response to "get channel access" command. 351 | 352 | Send dummy packet to response "get channel access" command. 353 | Just exists to be able to negotiate with vCenter Server. 354 | """ 355 | data = [ 356 | 0b00100010, # alerting disabled, auth enabled, always available 357 | 0x04, # priviredge level limit = administrator 358 | ] 359 | session.send_ipmi_response(data=data) 360 | 361 | def get_channel_info(self, request, session): 362 | """Fake response to "get channel access" command. 363 | 364 | Send dummy packet to response "get channel access" command 365 | as 802.3 LAN channel. 366 | Just exists to be able to negotiate with vCenter Server. 367 | """ 368 | data = [ 369 | 0x02, # channel number = 2 370 | 0x04, # channel medium type = 802.3 LAN 371 | 0x01, # channel protocol type = IPMB-1.0 372 | 0x80, # session support = multi-session 373 | 0xF2, # vendor id = 7154 374 | 0x1B, # vendor id = 7154 375 | 0x00, # vendor id = 7154 376 | 0x00, # reserved 377 | 0x00, # reserved 378 | ] 379 | session.send_ipmi_response(data=data) 380 | 381 | def get_lan_configuration_parameters(self, request, session): 382 | """Fake response to "get lan conf params" command. 383 | 384 | Send dummy packet to response "get lan conf params" command 385 | with fake MAC address. 386 | Just exists to be able to negotiate with vCenter Server. 387 | """ 388 | data = [0] # the first byte is revision, force to 0 as a dummy 389 | 390 | req_param = request["data"][1] 391 | LOG.info("Requested parameter = %s" % req_param) 392 | 393 | if req_param == 5: # mac address 394 | data.extend(utils.convert_fakemac_string_to_bytes(self.fakemac)) 395 | else: 396 | pass 397 | 398 | LOG.info("ne: %s" % data) 399 | 400 | if len(data) > 1: 401 | session.send_ipmi_response(data=data) 402 | else: 403 | session.send_ipmi_response(data=data, code=0x80) 404 | 405 | def handle_raw_request(self, request, session): 406 | """Call the appropriate function depending on the received command. 407 | 408 | Based on pyghmi 1.5.16, Apache License 2.0 409 | https://opendev.org/x/pyghmi/src/branch/master/pyghmi/ipmi/bmc.py 410 | """ 411 | # | FNC:CMD | NetFunc | Command | 412 | # | --------- | ----------------|------------------------------------ | 413 | # | 0x00:0x00 | Chassis | Chassis Capabilities | 414 | # | 0x00:0x01 | Chassis | Get Chassis Status | 415 | # | 0x00:0x02 | Chassis | Chassis Control | 416 | # | 0x00:0x08 | Chassis | Set System Boot Options | 417 | # | 0x00:0x09 | Chassis | Get System Boot Options | 418 | # | 0x04:0x2D | Sensor/Event | Get Sensor Reading | 419 | # | 0x04:0x2F | Sensor/Event | Get Sensor Type | 420 | # | 0x04:0x30 | Sensor/Event | Set Sensor Reading and Event Status | 421 | # | 0x06:0x01 | App | Get Device ID | 422 | # | 0x06:0x02 | App | Cold Reset | 423 | # | 0x06:0x03 | App | Warm Reset | 424 | # | 0x06:0x04 | App | Get Self Test Results | 425 | # | 0x06:0x08 | App | Get Device GUID | 426 | # | 0x06:0x22 | App | Reset Watchdog Timer | 427 | # | 0x06:0x24 | App | Set Watchdog Timer | 428 | # | 0x06:0x2E | App | Set BMC Global Enables | 429 | # | 0x06:0x31 | App | Get Message Flags | 430 | # | 0x06:0x35 | App | Read Event Message Buffer | 431 | # | 0x06:0x36 | App | Get BT Interface Capabilities | 432 | # | 0x06:0x40 | App | Set Channel Access | 433 | # | 0x06:0x41 | App | Get Channel Access | 434 | # | 0x06:0x42 | App | Get Channel Info Command | 435 | # | 0x0A:0x10 | Storage | Get FRU Inventory Area Info | 436 | # | 0x0A:0x11 | Storage | Read FRU Data | 437 | # | 0x0A:0x12 | Storage | Write FRU Data | 438 | # | 0x0A:0x40 | Storage | Get SEL Info | 439 | # | 0x0A:0x42 | Storage | Reserve SEL | 440 | # | 0x0A:0x44 | Storage | Add SEL Entry | 441 | # | 0x0A:0x48 | Storage | Get SEL Time | 442 | # | 0x0A:0x49 | Storage | Set SEL Time | 443 | # | 0x0C:0x01 | Transport | Set LAN Configuration Parameters | 444 | # | 0x0C:0x02 | Transport | Get LAN Configuration Parameters | 445 | # | 0x2C:0x00 | Group Extension | Group Extension Command | 446 | # | 0x2C:0x03 | Group Extension | Get Power Limit | 447 | # | 0x2C:0x04 | Group Extension | Set Power Limit | 448 | # | 0x2C:0x05 | Group Extension | Activate/Deactivate Power Limit | 449 | # | 0x2C:0x06 | Group Extension | Get Asset Tag | 450 | # | 0x2C:0x08 | Group Extension | Set Asset Tag | 451 | LOG.info( 452 | "Received netfn = 0x%x (%d), command = 0x%x (%d), data = %s" 453 | % ( 454 | request["netfn"], 455 | request["netfn"], 456 | request["command"], 457 | request["command"], 458 | request["data"].hex(), 459 | ) 460 | ) 461 | try: 462 | if request["netfn"] == 6: 463 | if request["command"] == 1: # get device id 464 | return self.send_device_id(session) 465 | elif request["command"] == 2: # cold reset 466 | return session.send_ipmi_response(code=self.cold_reset()) 467 | elif request["command"] == 0x41: # get channel access 468 | return self.get_channel_access(request, session) 469 | elif request["command"] == 0x42: # get channel info 470 | return self.get_channel_info(request, session) 471 | elif request["command"] == 0x48: # activate payload 472 | return self.activate_payload(request, session) 473 | elif request["command"] == 0x49: # deactivate payload 474 | return self.deactivate_payload(request, session) 475 | elif request["netfn"] == 0: 476 | if request["command"] == 1: # get chassis status 477 | return self.get_chassis_status(session) 478 | elif request["command"] == 2: # chassis control 479 | return self.control_chassis(request, session) 480 | elif request["command"] == 8: # set boot options 481 | return self.set_system_boot_options(request, session) 482 | elif request["command"] == 9: # get boot options 483 | return self.get_system_boot_options(request, session) 484 | elif request["netfn"] == 12: 485 | if request["command"] == 2: # get lan configuration parameters 486 | return self.get_lan_configuration_parameters(request, session) 487 | session.send_ipmi_response(code=0xC1) 488 | except NotImplementedError: 489 | session.send_ipmi_response(code=0xC1) 490 | except Exception: 491 | session._send_ipmi_net_payload(code=0xFF) 492 | traceback.print_exc() 493 | --------------------------------------------------------------------------------