├── .coveragerc ├── .gitignore ├── .gitreview ├── .stestr.conf ├── CONTRIBUTING.rst ├── HACKING.rst ├── LICENSE ├── README.rst ├── bindep.txt ├── doc ├── requirements.txt └── source │ ├── conf.py │ ├── contributor │ └── index.rst │ ├── index.rst │ ├── install │ └── index.rst │ └── user │ └── index.rst ├── releasenotes ├── notes │ ├── .placeholder │ ├── add-client-server-overhaul-c5b6f8c01126b4a3.yaml │ ├── add-config-env-8287bea486821653.yaml │ ├── drop-py-2-7-afe69612bfabaeee.yaml │ ├── fix-hanging-on-pipe-7c4b5f9c81623b524.yaml │ ├── ignore-start-if-running-c9a8f6c0514624a1.yaml │ ├── preserve-libvirt-domain-info-955410f570060241.yaml │ ├── py36-37-bye-bye-f3268421bf6c5bb4.yaml │ ├── remove-py38-77754bb686155b4b.yaml │ ├── remove-vbmcd-autostart-d1f567803526a4c1.yaml │ └── show-error-status-13456782b3a5a6e2.yaml └── source │ ├── _static │ └── .placeholder │ ├── _templates │ └── .placeholder │ ├── conf.py │ ├── index.rst │ └── unreleased.rst ├── requirements.txt ├── setup.cfg ├── setup.py ├── test-requirements.txt ├── tox.ini ├── virtualbmc ├── __init__.py ├── cmd │ ├── __init__.py │ ├── vbmc.py │ └── vbmcd.py ├── config.py ├── control.py ├── exception.py ├── log.py ├── manager.py ├── tests │ ├── __init__.py │ └── unit │ │ ├── __init__.py │ │ ├── base.py │ │ ├── cmd │ │ ├── __init__.py │ │ ├── test_vbmc.py │ │ └── test_vbmcd.py │ │ ├── test_config.py │ │ ├── test_control.py │ │ ├── test_manager.py │ │ ├── test_utils.py │ │ ├── test_vbmc.py │ │ └── utils.py ├── utils.py └── vbmc.py └── zuul.d ├── project.yaml └── virtualbmc-jobs.yaml /.coveragerc: -------------------------------------------------------------------------------- 1 | [run] 2 | branch = True 3 | source = virtualbmc 4 | omit = virtualbmc/tests/* 5 | 6 | [report] 7 | ignore_errors = True 8 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.py[cod] 2 | 3 | # C extensions 4 | *.so 5 | 6 | # Packages 7 | *.egg 8 | *.egg-info 9 | dist 10 | build 11 | .eggs 12 | eggs 13 | parts 14 | bin 15 | var 16 | sdist 17 | develop-eggs 18 | .installed.cfg 19 | lib 20 | lib64 21 | 22 | # Installer logs 23 | pip-log.txt 24 | 25 | # Unit test / coverage reports 26 | .coverage 27 | cover 28 | .tox 29 | nosetests.xml 30 | .testrepository 31 | .stestr 32 | .venv 33 | 34 | # Translations 35 | *.mo 36 | 37 | # Files created by releasenotes build 38 | releasenotes/build 39 | 40 | # Mr Developer 41 | .mr.developer.cfg 42 | .project 43 | .pydevproject 44 | 45 | # Complexity 46 | output/*.html 47 | output/*/index.html 48 | 49 | # Sphinx 50 | doc/build 51 | 52 | # pbr generates these 53 | AUTHORS 54 | ChangeLog 55 | 56 | # Editors 57 | *~ 58 | .*.swp 59 | .*sw? 60 | -------------------------------------------------------------------------------- /.gitreview: -------------------------------------------------------------------------------- 1 | [gerrit] 2 | host=review.opendev.org 3 | port=29418 4 | project=openstack/virtualbmc.git 5 | -------------------------------------------------------------------------------- /.stestr.conf: -------------------------------------------------------------------------------- 1 | [DEFAULT] 2 | test_path=${TESTS_DIR:-./virtualbmc/tests/unit/} 3 | top_dir=./ 4 | -------------------------------------------------------------------------------- /CONTRIBUTING.rst: -------------------------------------------------------------------------------- 1 | If you would like to contribute to the development of OpenStack, you must 2 | follow the steps in this page: 3 | 4 | http://docs.openstack.org/infra/manual/developers.html 5 | 6 | If you already have a good understanding of how the system works and your 7 | OpenStack accounts are set up, you can skip to the development workflow 8 | section of this documentation to learn how changes to OpenStack should be 9 | submitted for review via the Gerrit tool: 10 | 11 | http://docs.openstack.org/infra/manual/developers.html#development-workflow 12 | 13 | Pull requests submitted through GitHub will be ignored. 14 | 15 | Bugs should be filed in Launchpad, not GitHub: 16 | 17 | https://bugs.launchpad.net/virtualbmc 18 | -------------------------------------------------------------------------------- /HACKING.rst: -------------------------------------------------------------------------------- 1 | virtualbmc Style Commandments 2 | =============================================== 3 | 4 | Read the OpenStack Style Commandments https://docs.openstack.org/hacking/latest/ 5 | -------------------------------------------------------------------------------- /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.rst: -------------------------------------------------------------------------------- 1 | ========== 2 | VirtualBMC 3 | ========== 4 | 5 | Team and repository tags 6 | ------------------------ 7 | 8 | .. image:: https://governance.openstack.org/tc/badges/virtualbmc.svg 9 | :target: https://governance.openstack.org/tc/reference/tags/index.html 10 | 11 | Overview 12 | -------- 13 | 14 | A virtual BMC for controlling virtual machines using IPMI commands. 15 | 16 | This software is intended for CI and development use only. Please do not run 17 | VirtualBMC in a production environment for any reason. 18 | 19 | Installation 20 | ~~~~~~~~~~~~ 21 | 22 | .. code-block:: bash 23 | 24 | pip install virtualbmc 25 | 26 | 27 | Supported IPMI commands 28 | ~~~~~~~~~~~~~~~~~~~~~~~ 29 | 30 | .. code-block:: bash 31 | 32 | # Power the virtual machine on, off, graceful off, NMI and reset 33 | ipmitool -I lanplus -U admin -P password -H 127.0.0.1 power on|off|soft|diag|reset 34 | 35 | # Check the power status 36 | ipmitool -I lanplus -U admin -P password -H 127.0.0.1 power status 37 | 38 | # Set the boot device to network, hd or cdrom 39 | ipmitool -I lanplus -U admin -P password -H 127.0.0.1 chassis bootdev pxe|disk|cdrom 40 | 41 | # Get the current boot device 42 | ipmitool -I lanplus -U admin -P password -H 127.0.0.1 chassis bootparam get 5 43 | 44 | Project resources 45 | ~~~~~~~~~~~~~~~~~ 46 | 47 | * Documentation: https://docs.openstack.org/virtualbmc/latest 48 | * Source: https://opendev.org/openstack/virtualbmc 49 | * Bugs: https://bugs.launchpad.net/virtualbmc 50 | * Release Notes: https://docs.openstack.org/releasenotes/virtualbmc/ 51 | 52 | For information on how to contribute to VirtualBMC, see 53 | https://docs.openstack.org/virtualbmc/latest/contributor 54 | 55 | -------------------------------------------------------------------------------- /bindep.txt: -------------------------------------------------------------------------------- 1 | # these are needed to compile Python dependencies from sources 2 | python3-all-dev [platform:dpkg !platform:ubuntu-precise test compile] 3 | python3-devel [platform:rpm test compile] 4 | build-essential [platform:dpkg test compile] 5 | libssl-dev [platform:dpkg test compile] 6 | libvirt-dev [platform:dpkg test compile] 7 | libvirt-devel [platform:rpm test compile] 8 | libzmq5 [platform:dpkg test compile] 9 | pkg-config [compile test] 10 | -------------------------------------------------------------------------------- /doc/requirements.txt: -------------------------------------------------------------------------------- 1 | reno>=3.1.0 # Apache-2.0 2 | sphinx>=2.0.0,!=2.1.0 # BSD 3 | openstackdocstheme>=2.2.1 # Apache-2.0 4 | sphinxcontrib-svg2pdfconverter>=0.1.0 # BSD 5 | -------------------------------------------------------------------------------- /doc/source/conf.py: -------------------------------------------------------------------------------- 1 | # Licensed under the Apache License, Version 2.0 (the "License"); 2 | # you may not use this file except in compliance with the License. 3 | # You may obtain 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, 9 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 10 | # implied. 11 | # See the License for the specific language governing permissions and 12 | # limitations under the License. 13 | 14 | import os 15 | import sys 16 | 17 | sys.path.insert(0, os.path.abspath('../..')) 18 | # -- General configuration ---------------------------------------------------- 19 | 20 | # Add any Sphinx extension module names here, as strings. They can be 21 | # extensions coming with Sphinx (named 'sphinx.ext.*') or your custom ones. 22 | extensions = [ 23 | 'sphinx.ext.autodoc', 24 | ] 25 | 26 | try: 27 | import openstackdocstheme 28 | extensions.append('openstackdocstheme') 29 | except ImportError: 30 | openstackdocstheme = None 31 | 32 | # openstackdocstheme options 33 | openstackdocs_repo_name = 'openstack/virtualbmc' 34 | openstackdocs_pdf_link = True 35 | openstackdocs_use_storyboard = False 36 | 37 | # autodoc generation is a bit aggressive and a nuisance when doing heavy 38 | # text edit cycles. 39 | # execute "export SPHINX_DEBUG=1" in your terminal to disable 40 | 41 | # The suffix of source filenames. 42 | source_suffix = '.rst' 43 | 44 | # The master toctree document. 45 | master_doc = 'index' 46 | 47 | # General information about the project. 48 | copyright = '2016, OpenStack Foundation' 49 | 50 | # If true, '()' will be appended to :func: etc. cross-reference text. 51 | add_function_parentheses = True 52 | 53 | # If true, the current module name will be prepended to all description 54 | # unit titles (such as .. function::). 55 | add_module_names = True 56 | 57 | # The name of the Pygments (syntax highlighting) style to use. 58 | pygments_style = 'native' 59 | 60 | # -- Options for HTML output -------------------------------------------------- 61 | 62 | # The theme to use for HTML and HTML Help pages. Major themes that come with 63 | # Sphinx are currently 'default' and 'sphinxdoc'. 64 | # html_theme_path = ["."] 65 | # html_theme = '_theme' 66 | # html_static_path = ['static'] 67 | if openstackdocstheme is not None: 68 | html_theme = 'openstackdocs' 69 | else: 70 | html_theme = 'default' 71 | 72 | # Output file base name for HTML help builder. 73 | htmlhelp_basename = 'virtualbmcdoc' 74 | 75 | # Grouping the document tree into LaTeX files. List of tuples 76 | # (source start file, target name, title, author, documentclass 77 | # [howto/manual]). 78 | latex_documents = [ 79 | ('index', 80 | 'doc-virtualbmc.tex', 81 | 'VirtualBMC Documentation', 82 | 'OpenStack Foundation', 'manual'), 83 | ] 84 | 85 | # Example configuration for intersphinx: refer to the Python standard library. 86 | #intersphinx_mapping = {'http://docs.python.org/': None} 87 | -------------------------------------------------------------------------------- /doc/source/contributor/index.rst: -------------------------------------------------------------------------------- 1 | ============ 2 | Contributing 3 | ============ 4 | .. include:: ../../../CONTRIBUTING.rst 5 | -------------------------------------------------------------------------------- /doc/source/index.rst: -------------------------------------------------------------------------------- 1 | .. virtualbmc documentation master file, created by 2 | sphinx-quickstart on Tue Jul 9 22:26:36 2013. 3 | You can adapt this file completely to your liking, but it should at least 4 | contain the root `toctree` directive. 5 | 6 | Welcome to VirtualBMC's documentation! 7 | ====================================== 8 | 9 | The VirtualBMC tool simulates a 10 | `Baseboard Management Controller `_ 11 | (BMC) by exposing 12 | `IPMI `_ 13 | responder to the network and talking to 14 | `libvirt `_ 15 | at the host vBMC is running at to manipulate virtual machines which pretend 16 | to be bare metal servers. 17 | 18 | Contents: 19 | 20 | .. toctree:: 21 | :maxdepth: 1 22 | 23 | install/index 24 | user/index 25 | contributor/index 26 | -------------------------------------------------------------------------------- /doc/source/install/index.rst: -------------------------------------------------------------------------------- 1 | ============ 2 | Installation 3 | ============ 4 | 5 | At the command line:: 6 | 7 | $ pip install virtualbmc 8 | 9 | Or, if you have virtualenvwrapper installed:: 10 | 11 | $ mkvirtualenv virtualbmc 12 | $ pip install virtualbmc 13 | -------------------------------------------------------------------------------- /doc/source/user/index.rst: -------------------------------------------------------------------------------- 1 | 2 | How to use VirtualBMC 3 | ===================== 4 | 5 | For the VirtualBMC tool to operate you first need to create libvirt 6 | domain(s) for example, via ``virsh``. 7 | 8 | The VirtualBMC tool is a client-server system where ``vbmcd`` server 9 | does all the heavy-lifting (speaks IPMI, calls libvirt) while ``vbmc`` 10 | client is merely a command-line tool sending commands to the server and 11 | rendering responses to the user. 12 | 13 | Both tools can make use of an optional configuration file, which is 14 | looked for in the following locations (in this order): 15 | 16 | * ``VIRTUALBMC_CONFIG`` environment variable pointing to a file 17 | * ``$HOME/.vbmc/virtualbmc.conf`` file 18 | * ``/etc/virtualbmc/virtualbmc.conf`` file 19 | 20 | If no configuration file has been found, the internal defaults apply. 21 | 22 | You should set up your systemd to launch the ``vbmcd`` server on system 23 | start up or you can just run ``vbmcd`` from command line if you do not need 24 | the tool running persistently on the system. Once the server is up and 25 | running, you can use the ``vbmc`` tool to configure your libvirt domains as 26 | if they were physical hardware servers. 27 | 28 | The ``vbmc`` client can only communicate with ``vbmcd`` server if both are 29 | running on the same host. However ``vbmcd`` can manage libvirt domains 30 | remotely. 31 | 32 | By this moment you should be able to have the ``ipmitool`` managing 33 | VirtualBMC instances over the network. 34 | 35 | Configuring virtual servers 36 | --------------------------- 37 | 38 | Use the ``vbmc`` command-line tool to create, delete, list, start and 39 | stop virtual BMCs for the virtual machines being managed over IPMI. 40 | 41 | * In order to see all command options supported by the ``vbmc`` tool 42 | do:: 43 | 44 | $ vbmc --help 45 | 46 | 47 | It's also possible to list the options from a specific command. For 48 | example, in order to know what can be provided as part of the ``add`` 49 | command do:: 50 | 51 | $ vbmc add --help 52 | 53 | 54 | * Adding a new virtual BMC to control libvirt domain called ``node-0``:: 55 | 56 | $ vbmc add node-0 57 | 58 | 59 | * Adding a new virtual BMC to control libvirt domain called ``node-1`` 60 | that will listen for IPMI commands on port ``6230``:: 61 | 62 | $ vbmc add node-1 --port 6230 63 | 64 | 65 | Alternatively, libvirt can be configured to ssh into a remote machine 66 | and manage libvirt domain through ssh connection:: 67 | 68 | $ vbmc add node-1 --port 6230 \ 69 | --libvirt-uri qemu+ssh://username@192.168.122.1/system 70 | 71 | .. note:: 72 | 73 | Binding a network port number below 1025 is restricted and only users 74 | with privilege will be able to start a virtual BMC on those ports. 75 | 76 | 77 | * Starting the virtual BMC to control libvirt domain ``node-0``:: 78 | 79 | $ vbmc start node-0 80 | 81 | 82 | * Stopping the virtual BMC that controls libvirt domain ``node-0``:: 83 | 84 | $ vbmc stop node-0 85 | 86 | 87 | * Getting the list of virtual BMCs including their libvirt domains and 88 | IPMI network endpoints they are reachable at:: 89 | 90 | $ vbmc list 91 | +-------------+---------+---------+------+ 92 | | Domain name | Status | Address | Port | 93 | +-------------+---------+---------+------+ 94 | | node-0 | running | :: | 623 | 95 | | node-1 | running | :: | 6230 | 96 | +-------------+---------+---------+------+ 97 | 98 | * To view configuration information for a specific virtual BMC:: 99 | 100 | $ vbmc show node-0 101 | +-----------------------+----------------+ 102 | | Property | Value | 103 | +-----------------------+----------------+ 104 | | address | :: | 105 | | domain_name | node-0 | 106 | | libvirt_sasl_password | *** | 107 | | libvirt_sasl_username | None | 108 | | libvirt_uri | qemu:///system | 109 | | password | *** | 110 | | port | 623 | 111 | | status | running | 112 | | username | admin | 113 | +-----------------------+----------------+ 114 | 115 | 116 | Server simulation 117 | ----------------- 118 | 119 | Once the virtual BMC for a specific domain has been created and started 120 | you can then issue IPMI commands against the address and port of that 121 | virtual BMC to control the libvirt domain. For example: 122 | 123 | * To power on the virtual machine:: 124 | 125 | $ ipmitool -I lanplus -U admin -P password -H 127.0.0.1 -p 6230 power on 126 | 127 | * To check its power status:: 128 | 129 | $ ipmitool -I lanplus -U admin -P password -H 127.0.0.1 -p 6230 power status 130 | 131 | * To set the boot device to disk:: 132 | 133 | $ ipmitool -I lanplus -U admin -P password -H 127.0.0.1 -p 6230 chassis bootdev disk 134 | 135 | * To get the current boot device:: 136 | 137 | $ ipmitool -I lanplus -U admin -P password -H 127.0.0.1 -p 6230 chassis bootparam get 5 138 | 139 | Backward compatible behaviour 140 | ----------------------------- 141 | 142 | In the past the ``vbmc`` tool was the only part of the vBMC system. To help 143 | users keeping their existing server-less workflows, the ``vbmc`` tool 144 | attempts to spawn the ``vbmcd`` piece whenever it figures server is not 145 | running. 146 | 147 | .. warning:: 148 | 149 | The backward compabible behaviour will be removed in two-cycle time past 150 | Queens. 151 | -------------------------------------------------------------------------------- /releasenotes/notes/.placeholder: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/openstack/virtualbmc/6e14e8bdb8cc022d843bfb98377bfc89d99fc9c5/releasenotes/notes/.placeholder -------------------------------------------------------------------------------- /releasenotes/notes/add-client-server-overhaul-c5b6f8c01126b4a3.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | features: 3 | - | 4 | Changes the design of the VirtualBMC tool. Instead of forking the 5 | ``vbmc`` command-line tool to become a daemon and serve a single 6 | libvirt domain, the ``vbmcd`` master process and ``vbmc`` command-line 7 | client have been introduced. These client-server tools communicate 8 | over the ZeroMQ queue. The ``vbmcd`` process is responsible for 9 | herding its children, each child still serves a single libvirt 10 | domain. 11 | - | 12 | The ``vbmc start`` command now supports multiple domains. 13 | upgrade: 14 | - | 15 | It is advised to invoke ``vbmcd`` master process on system boot, 16 | perhaps by a systemd unit file. 17 | deprecations: 18 | - | 19 | Deprecates automatically starting up the ``vbmcd`` daemon process if 20 | it is not running. This backward-compatibility feature will be removed 21 | in the OpenStack Stein release. 22 | security: 23 | - | 24 | Hardens PID file creation to prevent the symlink attack. 25 | -------------------------------------------------------------------------------- /releasenotes/notes/add-config-env-8287bea486821653.yaml: -------------------------------------------------------------------------------- 1 | features: 2 | - | 3 | Adds the ability to override default configuration file location by 4 | exporting the ``$VIRTUALBMC_CONFIG`` variable, pointing to the desired 5 | config file, into ``vbmcd`` and ``vbmc`` processes environment. 6 | -------------------------------------------------------------------------------- /releasenotes/notes/drop-py-2-7-afe69612bfabaeee.yaml: -------------------------------------------------------------------------------- 1 | upgrade: 2 | - | 3 | Python 2.7 support has been dropped. Last release of virtualbmc 4 | to support Python 2.7 is OpenStack Train. The minimum version of Python now 5 | supported by virtualbmc is Python 3.6. 6 | -------------------------------------------------------------------------------- /releasenotes/notes/fix-hanging-on-pipe-7c4b5f9c81623b524.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | fixes: 3 | - | 4 | Properly closes standard I/O streams to prevent shell-piped processes from 5 | hanging infinitely on a dead pipe. 6 | -------------------------------------------------------------------------------- /releasenotes/notes/ignore-start-if-running-c9a8f6c0514624a1.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | fixes: 3 | - | 4 | Ignores instance "start" command if instance appears to be running. This 5 | helps preserving backward-compatible behaviour, as previous implementation 6 | has required the user to explicitly "start" enabled instances. With current 7 | virtualbmc, only the master process needs to be started. 8 | -------------------------------------------------------------------------------- /releasenotes/notes/preserve-libvirt-domain-info-955410f570060241.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | security: 3 | - | 4 | Secure information in the Libvirt domain XML document is now preserved. 5 | For more information, please see `story 2010382 6 | `_. 7 | fixes: 8 | - | 9 | Fixes an issue where secure fields were accidentally lost in the Libvirt 10 | domain XML document. 11 | -------------------------------------------------------------------------------- /releasenotes/notes/py36-37-bye-bye-f3268421bf6c5bb4.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | upgrade: 3 | - | 4 | Virtualbmc does not support Python 3.6 and 3.7 anymore, please use 5 | version 3.8 or higher. 6 | 7 | -------------------------------------------------------------------------------- /releasenotes/notes/remove-py38-77754bb686155b4b.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | upgrade: 3 | - | 4 | Support for Python 3.8 has been removed. Now the minimum python version 5 | supported is 3.9 . 6 | -------------------------------------------------------------------------------- /releasenotes/notes/remove-vbmcd-autostart-d1f567803526a4c1.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | upgrade: 3 | - | 4 | Removes the backward compatibility feature of ``vbmc`` to automatically 5 | start up ``vbmcd`` daemon process if it is not running. From now on, 6 | ``vbmcd`` should be started by systemd or some other mechanism. 7 | -------------------------------------------------------------------------------- /releasenotes/notes/show-error-status-13456782b3a5a6e2.yaml: -------------------------------------------------------------------------------- 1 | features: 2 | - | 3 | Added ``error`` status to ``vbmc list`` and ``vbmc start`` commands 4 | output. If the instance is failing to start, such instance will be shown 5 | as ``error`` rather than being ``down``. 6 | -------------------------------------------------------------------------------- /releasenotes/source/_static/.placeholder: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/openstack/virtualbmc/6e14e8bdb8cc022d843bfb98377bfc89d99fc9c5/releasenotes/source/_static/.placeholder -------------------------------------------------------------------------------- /releasenotes/source/_templates/.placeholder: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/openstack/virtualbmc/6e14e8bdb8cc022d843bfb98377bfc89d99fc9c5/releasenotes/source/_templates/.placeholder -------------------------------------------------------------------------------- /releasenotes/source/conf.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # Licensed under the Apache License, Version 2.0 (the "License"); 3 | # you may not use this file except in compliance with the License. 4 | # You may obtain a copy of the License at 5 | # 6 | # http://www.apache.org/licenses/LICENSE-2.0 7 | # 8 | # Unless required by applicable law or agreed to in writing, software 9 | # distributed under the License is distributed on an "AS IS" BASIS, 10 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 11 | # implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | # VirtualBMC Release Notes documentation build configuration file, created by 16 | # sphinx-quickstart on Mon Jun 25 13:25:41 2018. 17 | # 18 | # This file is execfile()d with the current directory set to its 19 | # containing dir. 20 | # 21 | # Note that not all possible configuration values are present in this 22 | # autogenerated file. 23 | # 24 | # All configuration values have a default; values that are commented out 25 | # serve to show the default. 26 | 27 | # If extensions (or modules to document with autodoc) are in another directory, 28 | # add these directories to sys.path here. If the directory is relative to the 29 | # documentation root, use os.path.abspath to make it absolute, like shown here. 30 | # 31 | # import os 32 | # import sys 33 | # sys.path.insert(0, os.path.abspath('.')) 34 | 35 | 36 | # -- General configuration ------------------------------------------------ 37 | 38 | # If your documentation needs a minimal Sphinx version, state it here. 39 | # 40 | # needs_sphinx = '1.0' 41 | 42 | # Add any Sphinx extension module names here, as strings. They can be 43 | # extensions coming with Sphinx (named 'sphinx.ext.*') or your custom 44 | # ones. 45 | extensions = [ 46 | 'reno.sphinxext' 47 | ] 48 | 49 | try: 50 | import openstackdocstheme 51 | extensions.append('openstackdocstheme') 52 | except ImportError: 53 | openstackdocstheme = None 54 | 55 | # Add any paths that contain templates here, relative to this directory. 56 | templates_path = ['_templates'] 57 | 58 | # The suffix(es) of source filenames. 59 | # You can specify multiple suffix as a list of string: 60 | # 61 | # source_suffix = ['.rst', '.md'] 62 | source_suffix = '.rst' 63 | 64 | # The master toctree document. 65 | master_doc = 'index' 66 | 67 | # General information about the project. 68 | copyright = '2018, Ironic Developers' 69 | author = 'Ironic Developers' 70 | 71 | # openstackdocstheme options 72 | openstackdocs_repo_name = 'openstack/virtualbmc' 73 | openstackdocs_use_storyboard = False 74 | 75 | # The version info for the project you're documenting, acts as replacement for 76 | # |version| and |release|, also used in various other places throughout the 77 | # built documents. 78 | # 79 | # The short X.Y version. 80 | version = '' 81 | # The full version, including alpha/beta/rc tags. 82 | release = '' 83 | 84 | # The language for content autogenerated by Sphinx. Refer to documentation 85 | # for a list of supported languages. 86 | # 87 | # This is also used if you do content translation via gettext catalogs. 88 | # Usually you set "language" from the command line for these cases. 89 | language = "en" 90 | 91 | # List of patterns, relative to source directory, that match files and 92 | # directories to ignore when looking for source files. 93 | # This patterns also effect to html_static_path and html_extra_path 94 | exclude_patterns = ['_build', 'Thumbs.db', '.DS_Store'] 95 | 96 | # The name of the Pygments (syntax highlighting) style to use. 97 | pygments_style = 'native' 98 | 99 | # If true, `todo` and `todoList` produce output, else they produce nothing. 100 | todo_include_todos = False 101 | 102 | 103 | # -- Options for HTML output ---------------------------------------------- 104 | 105 | # The theme to use for HTML and HTML Help pages. See the documentation for 106 | # a list of builtin themes. 107 | # 108 | if openstackdocstheme is not None: 109 | html_theme = 'openstackdocs' 110 | else: 111 | html_theme = 'default' 112 | 113 | # Theme options are theme-specific and customize the look and feel of a theme 114 | # further. For a list of options available for each theme, see the 115 | # documentation. 116 | # 117 | # html_theme_options = {} 118 | 119 | # Add any paths that contain custom static files (such as style sheets) here, 120 | # relative to this directory. They are copied after the builtin static files, 121 | # so a file named "default.css" will overwrite the builtin "default.css". 122 | html_static_path = ['_static'] 123 | 124 | # Custom sidebar templates, must be a dictionary that maps document names 125 | # to template names. 126 | # 127 | # This is required for the alabaster theme 128 | # refs: http://alabaster.readthedocs.io/en/latest/installation.html#sidebars 129 | html_sidebars = { 130 | '**': [ 131 | 'relations.html', # needs 'show_related': True theme option to display 132 | 'searchbox.html', 133 | ] 134 | } 135 | 136 | 137 | # -- Options for HTMLHelp output ------------------------------------------ 138 | 139 | # Output file base name for HTML help builder. 140 | htmlhelp_basename = 'VirtualBMCReleaseNotesdoc' 141 | 142 | 143 | # -- Options for LaTeX output --------------------------------------------- 144 | 145 | # Grouping the document tree into LaTeX files. List of tuples 146 | # (source start file, target name, title, 147 | # author, documentclass [howto, manual, or own class]). 148 | latex_documents = [ 149 | (master_doc, 'VirtualBMCReleaseNotes.tex', 150 | 'VirtualBMC Release Notes Documentation', 151 | 'Ironic Developers', 'manual'), 152 | ] 153 | 154 | 155 | # -- Options for manual page output --------------------------------------- 156 | 157 | # One entry per manual page. List of tuples 158 | # (source start file, name, description, authors, manual section). 159 | man_pages = [ 160 | (master_doc, 'virtualbmcreleasenotes', 161 | 'VirtualBMC Release Notes Documentation', 162 | [author], 1) 163 | ] 164 | 165 | 166 | # -- Options for Texinfo output ------------------------------------------- 167 | 168 | # Grouping the document tree into Texinfo files. List of tuples 169 | # (source start file, target name, title, author, 170 | # dir menu entry, description, category) 171 | texinfo_documents = [ 172 | (master_doc, 'VirtualBMCReleaseNotes', 173 | 'VirtualBMC Release Notes Documentation', 174 | author, 'VirtualBMCReleaseNotes', 'One line description of project.', 175 | 'Miscellaneous'), 176 | ] 177 | 178 | # -- Options for Internationalization output ------------------------------ 179 | locale_dirs = ['locale/'] 180 | -------------------------------------------------------------------------------- /releasenotes/source/index.rst: -------------------------------------------------------------------------------- 1 | ======================== 2 | VirtualBMC Release Notes 3 | ======================== 4 | 5 | .. toctree:: 6 | :maxdepth: 1 7 | 8 | unreleased 9 | -------------------------------------------------------------------------------- /releasenotes/source/unreleased.rst: -------------------------------------------------------------------------------- 1 | ============================== 2 | Current Series Release Notes 3 | ============================== 4 | 5 | .. release-notes:: 6 | -------------------------------------------------------------------------------- /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 | libvirt-python>=6.0.0 # LGPLv2+ 7 | pyghmi>=1.2.0 # Apache-2.0 8 | cliff!=2.9.0,>=2.8.0 # Apache-2.0 9 | pyzmq>=19.0.0 # LGPL+BSD 10 | -------------------------------------------------------------------------------- /setup.cfg: -------------------------------------------------------------------------------- 1 | [metadata] 2 | name = virtualbmc 3 | summary = Create virtual BMCs for controlling virtual instances via IPMI 4 | description_file = 5 | README.rst 6 | author = OpenStack 7 | author_email = openstack-discuss@lists.openstack.org 8 | home_page = https://docs.openstack.org/virtualbmc/latest/ 9 | python_requires = >=3.9 10 | classifier = 11 | Environment :: OpenStack 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.9 21 | Programming Language :: Python :: 3.10 22 | Programming Language :: Python :: 3.11 23 | Programming Language :: Python :: 3.12 24 | 25 | [files] 26 | packages = 27 | virtualbmc 28 | 29 | [entry_points] 30 | console_scripts = 31 | vbmc = virtualbmc.cmd.vbmc:main 32 | vbmcd = virtualbmc.cmd.vbmcd:main 33 | 34 | virtualbmc = 35 | add = virtualbmc.cmd.vbmc:AddCommand 36 | delete = virtualbmc.cmd.vbmc:DeleteCommand 37 | start = virtualbmc.cmd.vbmc:StartCommand 38 | stop = virtualbmc.cmd.vbmc:StopCommand 39 | list = virtualbmc.cmd.vbmc:ListCommand 40 | show = virtualbmc.cmd.vbmc:ShowCommand 41 | 42 | [codespell] 43 | quiet-level = 4 44 | # Words to ignore: 45 | # cna: Intel CNA card 46 | ignore-words-list = cna 47 | -------------------------------------------------------------------------------- /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( 19 | setup_requires=['pbr>=2.0.0'], 20 | pbr=True) 21 | -------------------------------------------------------------------------------- /test-requirements.txt: -------------------------------------------------------------------------------- 1 | coverage>=4.0 # Apache-2.0 2 | python-subunit>=1.0.0 # Apache-2.0/BSD 3 | oslotest>=3.2.0 # Apache-2.0 4 | testrepository>=0.0.18 # Apache-2.0/BSD 5 | testscenarios>=0.4 # Apache-2.0/BSD 6 | testtools>=2.2.0 # MIT 7 | stestr>=2.0.0 # Apache-2.0 8 | -------------------------------------------------------------------------------- /tox.ini: -------------------------------------------------------------------------------- 1 | [tox] 2 | minversion = 4.4.0 3 | envlist = py3,pep8 4 | ignore_basepython_conflict=true 5 | 6 | [testenv] 7 | constrain_package_deps = true 8 | usedevelop = True 9 | setenv = VIRTUAL_ENV={envdir} 10 | PYTHONDONTWRITEBYTECODE = 1 11 | LANGUAGE=en_US 12 | LC_ALL=en_US.UTF-8 13 | TESTS_DIR=./virtualbmc/tests/unit/ 14 | deps = 15 | -c{env:TOX_CONSTRAINTS_FILE:https://releases.openstack.org/constraints/upper/master} 16 | -r{toxinidir}/test-requirements.txt 17 | -r{toxinidir}/requirements.txt 18 | commands = 19 | stestr run {posargs} 20 | stestr slowest 21 | passenv = 22 | http_proxy 23 | HTTP_PROXY 24 | https_proxy 25 | HTTPS_PROXY 26 | no_proxy 27 | NO_PROXY 28 | 29 | [testenv:pep8] 30 | deps= 31 | hacking~=6.1.0 # Apache-2.0 32 | flake8-import-order~=0.18.0 # LGPLv3 33 | pycodestyle>=2.0.0,<3.0.0 # MIT 34 | doc8~=1.1.0 # Apache-2.0 35 | commands = 36 | flake8 {posargs} 37 | doc8 README.rst CONTRIBUTING.rst HACKING.rst doc/source 38 | 39 | [testenv:venv] 40 | commands = {posargs} 41 | 42 | [testenv:cover] 43 | setenv = {[testenv]setenv} 44 | PYTHON=coverage run --source virtualbmc --parallel-mode 45 | commands = 46 | stestr run {posargs} 47 | coverage combine 48 | coverage html -d cover 49 | coverage xml -o cover/coverage.xml 50 | 51 | [testenv:docs] 52 | deps = -c{env:TOX_CONSTRAINTS_FILE:https://releases.openstack.org/constraints/upper/master} 53 | -r{toxinidir}/doc/requirements.txt 54 | commands = sphinx-build -W -b html doc/source doc/build/html 55 | 56 | [testenv:pdf-docs] 57 | allowlist_externals = make 58 | deps = {[testenv:docs]deps} 59 | commands = sphinx-build -b latex doc/source doc/build/pdf 60 | make -C doc/build/pdf 61 | 62 | [testenv:releasenotes] 63 | deps = {[testenv:docs]deps} 64 | commands = 65 | sphinx-build -a -E -W -d releasenotes/build/doctrees -b html releasenotes/source releasenotes/build/html 66 | 67 | [testenv:debug] 68 | commands = oslo_debug_helper {posargs} 69 | 70 | [flake8] 71 | # [E129] Visually indented line with same indent as next logical line. 72 | # [W503] Line break occurred before a binary operator. Conflicts with W504. 73 | ignore = E129,W503 74 | filename = *.py 75 | import-order-style = pep8 76 | application-import-names = virtualbmc 77 | exclude = .venv,.git,.tox,dist,doc,*lib/python*,*egg,build 78 | max-complexity=17 79 | 80 | [testenv:codespell] 81 | description = 82 | Run codespell to check spelling 83 | deps = codespell 84 | # note(JayF): {posargs} lets us run `tox -ecodespell -- -w` to get codespell 85 | # to correct spelling issues in our code it's aware of. 86 | commands = 87 | codespell {posargs} -------------------------------------------------------------------------------- /virtualbmc/__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('virtualbmc').version_string() 16 | -------------------------------------------------------------------------------- /virtualbmc/cmd/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/openstack/virtualbmc/6e14e8bdb8cc022d843bfb98377bfc89d99fc9c5/virtualbmc/cmd/__init__.py -------------------------------------------------------------------------------- /virtualbmc/cmd/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 json 14 | import logging 15 | import sys 16 | 17 | from cliff.app import App 18 | from cliff.command import Command 19 | from cliff.commandmanager import CommandManager 20 | from cliff.lister import Lister 21 | import zmq 22 | 23 | import virtualbmc 24 | from virtualbmc import config as vbmc_config 25 | from virtualbmc.exception import VirtualBMCError 26 | from virtualbmc import log 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 {attr: getattr(obj, attr) 54 | for attr in dir(obj) if not attr.startswith('_')} 55 | 56 | def communicate(self, command, args, no_daemon=False): 57 | 58 | data_out = self.to_dict(args) 59 | 60 | data_out.update(command=command) 61 | 62 | data_out = json.dumps(data_out) 63 | 64 | server_port = CONF['default']['server_port'] 65 | 66 | context = socket = None 67 | 68 | try: 69 | context = zmq.Context() 70 | socket = context.socket(zmq.REQ) 71 | socket.setsockopt(zmq.LINGER, 5) 72 | socket.connect("tcp://127.0.0.1:%s" % server_port) 73 | 74 | poller = zmq.Poller() 75 | poller.register(socket, zmq.POLLIN) 76 | 77 | try: 78 | socket.send(data_out.encode('utf-8')) 79 | 80 | socks = dict(poller.poll(timeout=self.SERVER_TIMEOUT)) 81 | if socket in socks and socks[socket] == zmq.POLLIN: 82 | data_in = socket.recv() 83 | 84 | else: 85 | raise zmq.ZMQError( 86 | zmq.RCVTIMEO, msg='Server response timed out') 87 | 88 | except zmq.ZMQError as ex: 89 | msg = ('Failed to connect to the vbmcd server on port ' 90 | '%(port)s, error: %(error)s' % {'port': server_port, 91 | 'error': ex}) 92 | LOG.error(msg) 93 | raise VirtualBMCError(msg) 94 | 95 | finally: 96 | if socket: 97 | socket.close() 98 | context.destroy() 99 | 100 | try: 101 | data_in = json.loads(data_in.decode('utf-8')) 102 | 103 | except ValueError as ex: 104 | msg = 'Server response parsing error %(error)s' % {'error': ex} 105 | LOG.error(msg) 106 | raise VirtualBMCError(msg) 107 | 108 | rc = data_in.pop('rc', None) 109 | if rc: 110 | msg = '(%(rc)s): %(msg)s' % { 111 | 'rc': rc, 112 | 'msg': '\n'.join(data_in.get('msg', ())) 113 | } 114 | LOG.error(msg) 115 | raise VirtualBMCError(msg) 116 | 117 | return data_in 118 | 119 | 120 | class AddCommand(Command): 121 | """Create a new BMC for a virtual machine instance""" 122 | 123 | def get_parser(self, prog_name): 124 | parser = super(AddCommand, self).get_parser(prog_name) 125 | 126 | parser.add_argument('domain_name', 127 | help='The name of the virtual machine') 128 | parser.add_argument('--username', 129 | dest='username', 130 | default='admin', 131 | help='The BMC username; defaults to "admin"') 132 | parser.add_argument('--password', 133 | dest='password', 134 | default='password', 135 | help='The BMC password; defaults to "password"') 136 | parser.add_argument('--port', 137 | dest='port', 138 | type=int, 139 | default=623, 140 | help='Port to listen on; defaults to 623') 141 | parser.add_argument('--address', 142 | dest='address', 143 | default='::', 144 | help=('The address to bind to (IPv4 and IPv6 ' 145 | 'are supported); defaults to ::')) 146 | parser.add_argument('--libvirt-uri', 147 | dest='libvirt_uri', 148 | default="qemu:///system", 149 | help=('The libvirt URI; defaults to ' 150 | '"qemu:///system"')) 151 | parser.add_argument('--libvirt-sasl-username', 152 | dest='libvirt_sasl_username', 153 | default=None, 154 | help=('The libvirt SASL username; defaults to ' 155 | 'None')) 156 | parser.add_argument('--libvirt-sasl-password', 157 | dest='libvirt_sasl_password', 158 | default=None, 159 | help=('The libvirt SASL password; defaults to ' 160 | 'None')) 161 | return parser 162 | 163 | def take_action(self, args): 164 | 165 | log = logging.getLogger(__name__) 166 | 167 | # Check if the username and password were given for SASL 168 | sasl_user = args.libvirt_sasl_username 169 | sasl_pass = args.libvirt_sasl_password 170 | if any((sasl_user, sasl_pass)): 171 | if not all((sasl_user, sasl_pass)): 172 | msg = ("A password and username are required to use " 173 | "Libvirt's SASL authentication") 174 | log.error(msg) 175 | raise VirtualBMCError(msg) 176 | 177 | self.app.zmq.communicate( 178 | 'add', args, no_daemon=self.app.options.no_daemon 179 | ) 180 | 181 | 182 | class DeleteCommand(Command): 183 | """Delete a virtual BMC for a virtual machine instance""" 184 | 185 | def get_parser(self, prog_name): 186 | parser = super(DeleteCommand, self).get_parser(prog_name) 187 | 188 | parser.add_argument('domain_names', nargs='+', 189 | help='A list of virtual machine names') 190 | 191 | return parser 192 | 193 | def take_action(self, args): 194 | self.app.zmq.communicate('delete', args, self.app.options.no_daemon) 195 | 196 | 197 | class StartCommand(Command): 198 | """Start a virtual BMC for a virtual machine instance""" 199 | 200 | def get_parser(self, prog_name): 201 | parser = super(StartCommand, self).get_parser(prog_name) 202 | 203 | parser.add_argument('domain_names', nargs='+', 204 | help='A list of virtual machine names') 205 | 206 | return parser 207 | 208 | def take_action(self, args): 209 | self.app.zmq.communicate( 210 | 'start', args, no_daemon=self.app.options.no_daemon 211 | ) 212 | 213 | 214 | class StopCommand(Command): 215 | """Stop a virtual BMC for a virtual machine instance""" 216 | 217 | def get_parser(self, prog_name): 218 | parser = super(StopCommand, self).get_parser(prog_name) 219 | 220 | parser.add_argument('domain_names', nargs='+', 221 | help='A list of virtual machine names') 222 | 223 | return parser 224 | 225 | def take_action(self, args): 226 | self.app.zmq.communicate( 227 | 'stop', args, no_daemon=self.app.options.no_daemon 228 | ) 229 | 230 | 231 | class ListCommand(Lister): 232 | """List all virtual BMC instances""" 233 | 234 | def take_action(self, args): 235 | rsp = self.app.zmq.communicate( 236 | 'list', args, no_daemon=self.app.options.no_daemon 237 | ) 238 | return rsp['header'], sorted(rsp['rows']) 239 | 240 | 241 | class ShowCommand(Lister): 242 | """Show virtual BMC properties""" 243 | 244 | def get_parser(self, prog_name): 245 | parser = super(ShowCommand, self).get_parser(prog_name) 246 | 247 | parser.add_argument('domain_name', 248 | help='The name of the virtual machine') 249 | 250 | return parser 251 | 252 | def take_action(self, args): 253 | rsp = self.app.zmq.communicate( 254 | 'show', args, no_daemon=self.app.options.no_daemon 255 | ) 256 | return rsp['header'], sorted(rsp['rows']) 257 | 258 | 259 | class VirtualBMCApp(App): 260 | 261 | def __init__(self): 262 | super(VirtualBMCApp, self).__init__( 263 | description='Virtual Baseboard Management Controller (BMC) backed ' 264 | 'by virtual machines', 265 | version=virtualbmc.__version__, 266 | command_manager=CommandManager('virtualbmc'), 267 | deferred_help=True, 268 | ) 269 | 270 | def build_option_parser(self, description, version, argparse_kwargs=None): 271 | parser = super(VirtualBMCApp, self).build_option_parser( 272 | description, version, argparse_kwargs 273 | ) 274 | 275 | parser.add_argument('--no-daemon', 276 | action='store_true', 277 | help='Do not start vbmcd automatically') 278 | 279 | return parser 280 | 281 | def initialize_app(self, argv): 282 | self.zmq = ZmqClient() 283 | 284 | def clean_up(self, cmd, result, err): 285 | self.LOG.debug('clean_up %(name)s', {'name': cmd.__class__.__name__}) 286 | if err: 287 | self.LOG.debug('got an error: %(error)s', {'error': err}) 288 | 289 | 290 | def main(argv=sys.argv[1:]): 291 | vbmc_app = VirtualBMCApp() 292 | return vbmc_app.run(argv) 293 | 294 | 295 | if __name__ == '__main__': 296 | sys.exit(main()) 297 | -------------------------------------------------------------------------------- /virtualbmc/cmd/vbmcd.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 virtualbmc 19 | from virtualbmc import config as vbmc_config 20 | from virtualbmc import control 21 | from virtualbmc import log 22 | from virtualbmc import utils 23 | 24 | 25 | LOG = log.get_logger() 26 | 27 | CONF = vbmc_config.get_config() 28 | 29 | 30 | def main(argv=sys.argv[1:]): 31 | parser = argparse.ArgumentParser( 32 | prog='VirtualBMC server', 33 | description='A virtual BMC server for controlling virtual instances', 34 | ) 35 | parser.add_argument('--version', action='version', 36 | version=virtualbmc.__version__) 37 | parser.add_argument('--foreground', 38 | action='store_true', 39 | default=False, 40 | help='Do not daemonize') 41 | 42 | args = parser.parse_args(argv) 43 | 44 | pid_file = CONF['default']['pid_file'] 45 | 46 | try: 47 | with open(pid_file) as f: 48 | pid = int(f.read()) 49 | 50 | os.kill(pid, 0) 51 | 52 | except Exception: 53 | pass 54 | 55 | else: 56 | LOG.error('server PID #%(pid)d still running', {'pid': pid}) 57 | return 1 58 | 59 | def wrap_with_pidfile(func, pid): 60 | dir_name = os.path.dirname(pid_file) 61 | 62 | if not os.path.exists(dir_name): 63 | os.makedirs(dir_name, mode=0o700) 64 | 65 | try: 66 | with tempfile.NamedTemporaryFile(mode='w+t', dir=dir_name, 67 | delete=False) as f: 68 | f.write(str(pid)) 69 | os.rename(f.name, pid_file) 70 | 71 | func() 72 | 73 | except Exception as e: 74 | LOG.error('%(error)s', {'error': e}) 75 | return 1 76 | 77 | finally: 78 | try: 79 | os.unlink(pid_file) 80 | 81 | except Exception: 82 | pass 83 | 84 | if args.foreground: 85 | return wrap_with_pidfile(control.application, os.getpid()) 86 | 87 | else: 88 | with utils.detach_process() as pid: 89 | if pid > 0: 90 | return 0 91 | 92 | return wrap_with_pidfile(control.application, pid) 93 | 94 | 95 | if __name__ == '__main__': 96 | sys.exit(main()) 97 | -------------------------------------------------------------------------------- /virtualbmc/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 virtualbmc import utils 17 | 18 | __all__ = ['get_config'] 19 | 20 | _CONFIG_FILE_PATHS = ( 21 | os.environ.get('VIRTUALBMC_CONFIG', ''), 22 | os.path.join(os.path.expanduser('~'), '.vbmc', 'virtualbmc.conf'), 23 | '/etc/virtualbmc/virtualbmc.conf') 24 | 25 | CONFIG_FILE = next((x for x in _CONFIG_FILE_PATHS if os.path.exists(x)), '') 26 | 27 | CONFIG = None 28 | 29 | 30 | class VirtualBMCConfig(object): 31 | 32 | DEFAULTS = { 33 | 'default': { 34 | 'show_passwords': 'false', 35 | 'config_dir': os.path.join( 36 | os.path.expanduser('~'), '.vbmc' 37 | ), 38 | 'pid_file': os.path.join( 39 | os.path.expanduser('~'), '.vbmc', 'master.pid' 40 | ), 41 | 'server_port': 50891, 42 | 'server_response_timeout': 5000, # milliseconds 43 | 'server_spawn_wait': 3000, # milliseconds 44 | }, 45 | 'log': { 46 | 'logfile': None, 47 | 'debug': 'false' 48 | }, 49 | 'ipmi': { 50 | # Maximum time (in seconds) to wait for the data to come across 51 | 'session_timeout': 1 52 | }, 53 | } 54 | 55 | def initialize(self): 56 | config = configparser.ConfigParser() 57 | config.read(CONFIG_FILE) 58 | self._conf_dict = self._as_dict(config) 59 | self._validate() 60 | 61 | def _as_dict(self, config): 62 | conf_dict = self.DEFAULTS 63 | for section in config.sections(): 64 | if section not in conf_dict: 65 | conf_dict[section] = {} 66 | for key, val in config.items(section): 67 | conf_dict[section][key] = val 68 | 69 | return conf_dict 70 | 71 | def _validate(self): 72 | self._conf_dict['log']['debug'] = utils.str2bool( 73 | self._conf_dict['log']['debug']) 74 | 75 | self._conf_dict['default']['show_passwords'] = utils.str2bool( 76 | self._conf_dict['default']['show_passwords']) 77 | 78 | self._conf_dict['default']['server_port'] = int( 79 | self._conf_dict['default']['server_port']) 80 | 81 | self._conf_dict['default']['server_spawn_wait'] = int( 82 | self._conf_dict['default']['server_spawn_wait']) 83 | 84 | self._conf_dict['default']['server_response_timeout'] = int( 85 | self._conf_dict['default']['server_response_timeout']) 86 | 87 | self._conf_dict['ipmi']['session_timeout'] = int( 88 | self._conf_dict['ipmi']['session_timeout']) 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 | -------------------------------------------------------------------------------- /virtualbmc/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 virtualbmc import config as vbmc_config 23 | from virtualbmc import exception 24 | from virtualbmc import log 25 | from virtualbmc.manager import VirtualBMCManager 26 | 27 | CONF = vbmc_config.get_config() 28 | 29 | LOG = log.get_logger() 30 | 31 | TIMER_PERIOD = 3000 # milliseconds 32 | 33 | 34 | def main_loop(vbmc_manager, handle_command): 35 | """Server part of the CLI control interface 36 | 37 | Receives JSON messages from ZMQ socket, calls the command handler and 38 | sends JSON response back to the client. 39 | 40 | Client builds requests out of its command-line options which 41 | include the command (e.g. `start`, `list` etc) and command-specific 42 | options. 43 | 44 | Server handles the commands and responds with a JSON document which 45 | contains at least the `rc` and `msg` attributes, used to indicate the 46 | outcome of the command, and optionally 2-D table conveyed through the 47 | `header` and `rows` attributes pointing to lists of cell values. 48 | """ 49 | server_port = CONF['default']['server_port'] 50 | 51 | context = socket = None 52 | 53 | try: 54 | context = zmq.Context() 55 | socket = context.socket(zmq.REP) 56 | socket.setsockopt(zmq.LINGER, 5) 57 | socket.bind("tcp://127.0.0.1:%s" % server_port) 58 | 59 | poller = zmq.Poller() 60 | poller.register(socket, zmq.POLLIN) 61 | 62 | LOG.info('Started vBMC server on port %s', server_port) 63 | 64 | while True: 65 | socks = dict(poller.poll(timeout=TIMER_PERIOD)) 66 | if socket in socks and socks[socket] == zmq.POLLIN: 67 | message = socket.recv() 68 | else: 69 | vbmc_manager.periodic() 70 | continue 71 | 72 | try: 73 | data_in = json.loads(message.decode('utf-8')) 74 | 75 | except ValueError as ex: 76 | LOG.warning( 77 | 'Control server request deserialization error: ' 78 | '%(error)s', {'error': ex} 79 | ) 80 | continue 81 | 82 | LOG.debug('Command request data: %(request)s', 83 | {'request': data_in}) 84 | 85 | try: 86 | data_out = handle_command(vbmc_manager, data_in) 87 | 88 | except exception.VirtualBMCError as ex: 89 | msg = 'Command failed: %(error)s' % {'error': ex} 90 | LOG.error(msg) 91 | data_out = { 92 | 'rc': 1, 93 | 'msg': [msg] 94 | } 95 | 96 | LOG.debug('Command response data: %(response)s', 97 | {'response': data_out}) 98 | 99 | try: 100 | message = json.dumps(data_out) 101 | 102 | except ValueError as ex: 103 | LOG.warning( 104 | 'Control server response serialization error: ' 105 | '%(error)s', {'error': ex} 106 | ) 107 | continue 108 | 109 | socket.send(message.encode('utf-8')) 110 | 111 | finally: 112 | if socket: 113 | socket.close() 114 | if context: 115 | context.destroy() 116 | 117 | 118 | def command_dispatcher(vbmc_manager, data_in): 119 | """Control CLI command dispatcher 120 | 121 | Calls vBMC manager to execute commands, implements uniform 122 | dictionary-based interface to the caller. 123 | """ 124 | command = data_in.pop('command') 125 | 126 | LOG.debug('Running "%(cmd)s" command handler', {'cmd': command}) 127 | 128 | if command == 'add': 129 | 130 | # Check if the username and password were given for SASL 131 | sasl_user = data_in['libvirt_sasl_username'] 132 | sasl_pass = data_in['libvirt_sasl_password'] 133 | if any((sasl_user, sasl_pass)): 134 | if not all((sasl_user, sasl_pass)): 135 | error = ("A password and username are required to use " 136 | "Libvirt's SASL authentication") 137 | return {'msg': [error], 'rc': 1} 138 | 139 | rc, msg = vbmc_manager.add(**data_in) 140 | 141 | return { 142 | 'rc': rc, 143 | 'msg': [msg] if msg else [] 144 | } 145 | 146 | elif command == 'delete': 147 | data_out = [vbmc_manager.delete(domain_name) 148 | for domain_name in set(data_in['domain_names'])] 149 | return { 150 | 'rc': max(rc for rc, msg in data_out), 151 | 'msg': [msg for rc, msg in data_out if msg], 152 | } 153 | 154 | elif command == 'start': 155 | data_out = [vbmc_manager.start(domain_name) 156 | for domain_name in set(data_in['domain_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 == 'stop': 163 | data_out = [vbmc_manager.stop(domain_name) 164 | for domain_name in set(data_in['domain_names'])] 165 | return { 166 | 'rc': max(rc for rc, msg in data_out), 167 | 'msg': [msg for rc, msg in data_out if msg], 168 | } 169 | 170 | elif command == 'list': 171 | rc, tables = vbmc_manager.list() 172 | 173 | header = ('Domain name', 'Status', 'Address', 'Port') 174 | keys = ('domain_name', 'status', 'address', 'port') 175 | return { 176 | 'rc': rc, 177 | 'header': header, 178 | 'rows': [ 179 | [table.get(key, '?') for key in keys] for table in tables 180 | ] 181 | } 182 | 183 | elif command == 'show': 184 | rc, table = vbmc_manager.show(data_in['domain_name']) 185 | 186 | return { 187 | 'rc': rc, 188 | 'header': ('Property', 'Value'), 189 | 'rows': table, 190 | } 191 | 192 | else: 193 | return { 194 | 'rc': 1, 195 | 'msg': ['Unknown command'], 196 | } 197 | 198 | 199 | def application(): 200 | """vbmcd application entry point 201 | 202 | Initializes, serves and cleans up everything. 203 | """ 204 | vbmc_manager = VirtualBMCManager() 205 | 206 | vbmc_manager.periodic() 207 | 208 | def kill_children(*args): 209 | vbmc_manager.periodic(shutdown=True) 210 | sys.exit(0) 211 | 212 | # SIGTERM does not seem to propagate to multiprocessing 213 | signal.signal(signal.SIGTERM, kill_children) 214 | 215 | try: 216 | main_loop(vbmc_manager, command_dispatcher) 217 | except KeyboardInterrupt: 218 | LOG.info('Got keyboard interrupt, exiting') 219 | vbmc_manager.periodic(shutdown=True) 220 | except Exception as ex: 221 | LOG.error( 222 | 'Control server error: %(error)s', {'error': ex} 223 | ) 224 | vbmc_manager.periodic(shutdown=True) 225 | -------------------------------------------------------------------------------- /virtualbmc/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 DomainAlreadyExists(VirtualBMCError): 27 | message = 'Domain %(domain)s already exists' 28 | 29 | 30 | class DomainNotFound(VirtualBMCError): 31 | message = 'No domain with matching name %(domain)s was found' 32 | 33 | 34 | class LibvirtConnectionOpenError(VirtualBMCError): 35 | message = ('Fail to establish a connection with libvirt URI "%(uri)s". ' 36 | 'Error: %(error)s') 37 | 38 | 39 | class DetachProcessError(VirtualBMCError): 40 | message = ('Error when forking (detaching) the VirtualBMC process ' 41 | 'from its parent and session. Error: %(error)s') 42 | -------------------------------------------------------------------------------- /virtualbmc/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 virtualbmc import config 17 | 18 | __all__ = ['get_logger'] 19 | 20 | DEFAULT_LOG_FORMAT = ('%(asctime)s %(process)d %(levelname)s ' 21 | '%(name)s [-] %(message)s') 22 | LOGGER = None 23 | 24 | 25 | class VirtualBMCLogger(logging.Logger): 26 | 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'], 54 | logfile=log_conf['logfile']) 55 | 56 | return LOGGER 57 | -------------------------------------------------------------------------------- /virtualbmc/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 virtualbmc import config as vbmc_config 21 | from virtualbmc import exception 22 | from virtualbmc import log 23 | from virtualbmc import utils 24 | from virtualbmc.vbmc import VirtualBMC 25 | 26 | LOG = log.get_logger() 27 | 28 | # BMC status 29 | RUNNING = 'running' 30 | DOWN = 'down' 31 | ERROR = 'error' 32 | 33 | DEFAULT_SECTION = 'VirtualBMC' 34 | 35 | CONF = vbmc_config.get_config() 36 | 37 | 38 | class VirtualBMCManager(object): 39 | 40 | VBMC_OPTIONS = ['username', 'password', 'address', 'port', 41 | 'domain_name', 'libvirt_uri', 'libvirt_sasl_username', 42 | 'libvirt_sasl_password', 'active'] 43 | 44 | def __init__(self): 45 | super(VirtualBMCManager, self).__init__() 46 | self.config_dir = CONF['default']['config_dir'] 47 | self._running_domains = {} 48 | 49 | def _parse_config(self, domain_name): 50 | config_path = os.path.join(self.config_dir, domain_name, 'config') 51 | if not os.path.exists(config_path): 52 | raise exception.DomainNotFound(domain=domain_name) 53 | 54 | try: 55 | config = configparser.ConfigParser() 56 | config.read(config_path) 57 | 58 | bmc = {} 59 | for item in self.VBMC_OPTIONS: 60 | try: 61 | value = config.get(DEFAULT_SECTION, item) 62 | except configparser.NoOptionError: 63 | value = None 64 | 65 | bmc[item] = value 66 | 67 | # Port needs to be int 68 | bmc['port'] = config.getint(DEFAULT_SECTION, 'port') 69 | 70 | return bmc 71 | 72 | except OSError: 73 | raise exception.DomainNotFound(domain=domain_name) 74 | 75 | def _store_config(self, **options): 76 | config = configparser.ConfigParser() 77 | config.add_section(DEFAULT_SECTION) 78 | 79 | for option, value in options.items(): 80 | if value is not None: 81 | config.set(DEFAULT_SECTION, option, str(value)) 82 | 83 | config_path = os.path.join( 84 | self.config_dir, options['domain_name'], 'config' 85 | ) 86 | 87 | with open(config_path, 'w') as f: 88 | config.write(f) 89 | 90 | def _vbmc_enabled(self, domain_name, lets_enable=None, config=None): 91 | if not config: 92 | config = self._parse_config(domain_name) 93 | 94 | try: 95 | currently_enabled = utils.str2bool(config['active']) 96 | 97 | except Exception: 98 | currently_enabled = False 99 | 100 | if (lets_enable is not None 101 | and lets_enable != currently_enabled): 102 | config.update(active=lets_enable) 103 | self._store_config(**config) 104 | currently_enabled = lets_enable 105 | 106 | return currently_enabled 107 | 108 | def _sync_vbmc_states(self, shutdown=False): 109 | """Starts/stops vBMC instances 110 | 111 | Walks over vBMC instances configuration, starts 112 | enabled but dead instances, kills non-configured 113 | but alive ones. 114 | """ 115 | 116 | def vbmc_runner(bmc_config): 117 | # The manager process installs a signal handler for SIGTERM to 118 | # propagate it to children. Return to the default handler. 119 | signal.signal(signal.SIGTERM, signal.SIG_DFL) 120 | 121 | show_passwords = CONF['default']['show_passwords'] 122 | 123 | if show_passwords: 124 | show_options = bmc_config 125 | else: 126 | show_options = utils.mask_dict_password(bmc_config) 127 | 128 | try: 129 | vbmc = VirtualBMC(**bmc_config) 130 | 131 | except Exception as ex: 132 | LOG.exception( 133 | 'Error running vBMC with configuration ' 134 | '%(opts)s: %(error)s', {'opts': show_options, 135 | 'error': ex} 136 | ) 137 | return 138 | 139 | try: 140 | vbmc.listen(timeout=CONF['ipmi']['session_timeout']) 141 | 142 | except Exception as ex: 143 | LOG.exception( 144 | 'Shutdown vBMC for domain %(domain)s, cause ' 145 | '%(error)s', {'domain': show_options['domain_name'], 146 | 'error': ex} 147 | ) 148 | return 149 | 150 | for domain_name in os.listdir(self.config_dir): 151 | if not os.path.isdir( 152 | os.path.join(self.config_dir, domain_name) 153 | ): 154 | continue 155 | 156 | try: 157 | bmc_config = self._parse_config(domain_name) 158 | 159 | except exception.DomainNotFound: 160 | continue 161 | 162 | if shutdown: 163 | lets_enable = False 164 | else: 165 | lets_enable = self._vbmc_enabled( 166 | domain_name, config=bmc_config 167 | ) 168 | 169 | instance = self._running_domains.get(domain_name) 170 | 171 | if lets_enable: 172 | 173 | if not instance or not instance.is_alive(): 174 | 175 | instance = multiprocessing.Process( 176 | name='vbmcd-managing-domain-%s' % domain_name, 177 | target=vbmc_runner, 178 | args=(bmc_config,) 179 | ) 180 | 181 | instance.daemon = True 182 | instance.start() 183 | 184 | self._running_domains[domain_name] = instance 185 | 186 | LOG.info( 187 | 'Started vBMC instance for domain ' 188 | '%(domain)s', {'domain': domain_name} 189 | ) 190 | 191 | if not instance.is_alive(): 192 | LOG.debug( 193 | 'Found dead vBMC instance for domain %(domain)s ' 194 | '(rc %(rc)s)', {'domain': domain_name, 195 | 'rc': instance.exitcode} 196 | ) 197 | 198 | else: 199 | if instance: 200 | if instance.is_alive(): 201 | instance.terminate() 202 | LOG.info( 203 | 'Terminated vBMC instance for domain ' 204 | '%(domain)s', {'domain': domain_name} 205 | ) 206 | 207 | self._running_domains.pop(domain_name, None) 208 | 209 | def _show(self, domain_name): 210 | bmc_config = self._parse_config(domain_name) 211 | 212 | show_passwords = CONF['default']['show_passwords'] 213 | 214 | if show_passwords: 215 | show_options = bmc_config 216 | else: 217 | show_options = utils.mask_dict_password(bmc_config) 218 | 219 | instance = self._running_domains.get(domain_name) 220 | 221 | if instance and instance.is_alive(): 222 | show_options['status'] = RUNNING 223 | elif instance and not instance.is_alive(): 224 | show_options['status'] = ERROR 225 | else: 226 | show_options['status'] = DOWN 227 | 228 | return show_options 229 | 230 | def periodic(self, shutdown=False): 231 | self._sync_vbmc_states(shutdown) 232 | 233 | def add(self, username, password, port, address, domain_name, 234 | libvirt_uri, libvirt_sasl_username, libvirt_sasl_password, 235 | **kwargs): 236 | 237 | # check libvirt's connection and if domain exist prior to adding it 238 | utils.check_libvirt_connection_and_domain( 239 | libvirt_uri, domain_name, 240 | sasl_username=libvirt_sasl_username, 241 | sasl_password=libvirt_sasl_password) 242 | 243 | domain_path = os.path.join(self.config_dir, domain_name) 244 | 245 | try: 246 | os.makedirs(domain_path) 247 | except OSError as ex: 248 | if ex.errno == errno.EEXIST: 249 | return 1, str(ex) 250 | 251 | msg = ('Failed to create domain %(domain)s. ' 252 | 'Error: %(error)s' % {'domain': domain_name, 'error': ex}) 253 | LOG.error(msg) 254 | return 1, msg 255 | 256 | try: 257 | self._store_config(domain_name=domain_name, 258 | username=username, 259 | password=password, 260 | port=str(port), 261 | address=address, 262 | libvirt_uri=libvirt_uri, 263 | libvirt_sasl_username=libvirt_sasl_username, 264 | libvirt_sasl_password=libvirt_sasl_password, 265 | active=False) 266 | 267 | except Exception as ex: 268 | self.delete(domain_name) 269 | return 1, str(ex) 270 | 271 | return 0, '' 272 | 273 | def delete(self, domain_name): 274 | domain_path = os.path.join(self.config_dir, domain_name) 275 | if not os.path.exists(domain_path): 276 | raise exception.DomainNotFound(domain=domain_name) 277 | 278 | try: 279 | self.stop(domain_name) 280 | except exception.VirtualBMCError: 281 | pass 282 | 283 | shutil.rmtree(domain_path) 284 | 285 | return 0, '' 286 | 287 | def start(self, domain_name): 288 | try: 289 | bmc_config = self._parse_config(domain_name) 290 | 291 | except Exception as ex: 292 | return 1, str(ex) 293 | 294 | if domain_name in self._running_domains: 295 | 296 | self._sync_vbmc_states() 297 | 298 | if domain_name in self._running_domains: 299 | LOG.warning( 300 | 'BMC instance %(domain)s already running, ignoring ' 301 | '"start" command' % {'domain': domain_name}) 302 | return 0, '' 303 | 304 | try: 305 | self._vbmc_enabled(domain_name, 306 | config=bmc_config, 307 | lets_enable=True) 308 | 309 | except Exception as e: 310 | LOG.exception('Failed to start domain %s', domain_name) 311 | return 1, ('Failed to start domain %(domain)s. Error: ' 312 | '%(error)s' % {'domain': domain_name, 'error': e}) 313 | 314 | self._sync_vbmc_states() 315 | 316 | return 0, '' 317 | 318 | def stop(self, domain_name): 319 | try: 320 | self._vbmc_enabled(domain_name, lets_enable=False) 321 | 322 | except Exception as ex: 323 | LOG.exception('Failed to stop domain %s', domain_name) 324 | return 1, str(ex) 325 | 326 | self._sync_vbmc_states() 327 | 328 | return 0, '' 329 | 330 | def list(self): 331 | rc = 0 332 | tables = [] 333 | try: 334 | for domain in os.listdir(self.config_dir): 335 | if os.path.isdir(os.path.join(self.config_dir, domain)): 336 | tables.append(self._show(domain)) 337 | 338 | except OSError as e: 339 | if e.errno == errno.EEXIST: 340 | rc = 1 341 | 342 | return rc, tables 343 | 344 | def show(self, domain_name): 345 | return 0, list(self._show(domain_name).items()) 346 | -------------------------------------------------------------------------------- /virtualbmc/tests/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/openstack/virtualbmc/6e14e8bdb8cc022d843bfb98377bfc89d99fc9c5/virtualbmc/tests/__init__.py -------------------------------------------------------------------------------- /virtualbmc/tests/unit/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/openstack/virtualbmc/6e14e8bdb8cc022d843bfb98377bfc89d99fc9c5/virtualbmc/tests/unit/__init__.py -------------------------------------------------------------------------------- /virtualbmc/tests/unit/base.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | # Copyright 2010-2011 OpenStack Foundation 4 | # Copyright (c) 2013 Hewlett-Packard Development Company, L.P. 5 | # 6 | # Licensed under the Apache License, Version 2.0 (the "License"); you may 7 | # not use this file except in compliance with the License. You may obtain 8 | # a copy of the License at 9 | # 10 | # http://www.apache.org/licenses/LICENSE-2.0 11 | # 12 | # Unless required by applicable law or agreed to in writing, software 13 | # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 14 | # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 15 | # License for the specific language governing permissions and limitations 16 | # under the License. 17 | import logging 18 | 19 | from oslotest import base 20 | 21 | from virtualbmc import log as vbmc_log 22 | 23 | 24 | class TestCase(base.BaseTestCase): 25 | """Test case base class for all unit tests.""" 26 | def setUp(self): 27 | super(TestCase, self).setUp() 28 | self._level = vbmc_log.get_logger().getEffectiveLevel() 29 | vbmc_log.get_logger().setLevel(logging.DEBUG) 30 | self.addCleanup(lambda level: vbmc_log.get_logger().setLevel(level), 31 | self._level) 32 | -------------------------------------------------------------------------------- /virtualbmc/tests/unit/cmd/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/openstack/virtualbmc/6e14e8bdb8cc022d843bfb98377bfc89d99fc9c5/virtualbmc/tests/unit/cmd/__init__.py -------------------------------------------------------------------------------- /virtualbmc/tests/unit/cmd/test_vbmc.py: -------------------------------------------------------------------------------- 1 | # Copyright 2016 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 io 17 | import json 18 | import sys 19 | from unittest import mock 20 | 21 | import zmq 22 | 23 | from virtualbmc.cmd import vbmc 24 | from virtualbmc.tests.unit import base 25 | from virtualbmc.tests.unit import utils as test_utils 26 | 27 | 28 | @mock.patch.object(sys, 'exit', lambda _: None) 29 | class VBMCTestCase(base.TestCase): 30 | 31 | def setUp(self): 32 | super(VBMCTestCase, self).setUp() 33 | self.domain = test_utils.get_domain() 34 | 35 | @mock.patch.object(zmq, 'Context') 36 | @mock.patch.object(zmq, 'Poller') 37 | def test_server_timeout(self, mock_zmq_poller, mock_zmq_context): 38 | expected_rc = 1 39 | expected_output = ( 40 | 'Failed to connect to the vbmcd server on port 50891, error: ' 41 | 'Server response timed out\n') 42 | 43 | mock_zmq_poller = mock_zmq_poller.return_value 44 | mock_zmq_poller.poll.return_value = {} 45 | 46 | with mock.patch.object(sys, 'stderr', io.StringIO()) as output: 47 | rc = vbmc.main(['--no-daemon', 48 | 'add', '--username', 'ironic', 'bar']) 49 | 50 | self.assertEqual(expected_rc, rc) 51 | self.assertEqual(expected_output, output.getvalue()) 52 | 53 | @mock.patch.object(zmq, 'Context') 54 | @mock.patch.object(zmq, 'Poller') 55 | def test_main_add(self, mock_zmq_poller, mock_zmq_context): 56 | expected_rc = 0 57 | expected_output = '' 58 | 59 | srv_rsp = { 60 | 'rc': expected_rc, 61 | 'msg': ['OK'] 62 | } 63 | 64 | mock_zmq_context = mock_zmq_context.return_value 65 | mock_zmq_socket = mock_zmq_context.socket.return_value 66 | mock_zmq_socket.recv.return_value = json.dumps(srv_rsp).encode() 67 | mock_zmq_poller = mock_zmq_poller.return_value 68 | mock_zmq_poller.poll.return_value = { 69 | mock_zmq_socket: zmq.POLLIN 70 | } 71 | 72 | with mock.patch.object(sys, 'stdout', io.StringIO()) as output: 73 | rc = vbmc.main(['add', '--username', 'ironic', 'bar']) 74 | 75 | query = json.loads(mock_zmq_socket.send.call_args[0][0].decode()) 76 | 77 | expected_query = { 78 | 'command': 'add', 79 | 'address': '::', 80 | 'port': 623, 81 | 'libvirt_uri': 'qemu:///system', 82 | 'libvirt_sasl_username': None, 83 | 'libvirt_sasl_password': None, 84 | 'username': 'ironic', 85 | 'password': 'password', 86 | 'domain_name': 'bar', 87 | } 88 | 89 | self.assertEqual(expected_query, query) 90 | 91 | self.assertEqual(expected_rc, rc) 92 | self.assertEqual(expected_output, output.getvalue()) 93 | 94 | @mock.patch.object(zmq, 'Context') 95 | @mock.patch.object(zmq, 'Poller') 96 | def test_main_delete(self, mock_zmq_poller, mock_zmq_context): 97 | expected_rc = 0 98 | expected_output = '' 99 | 100 | srv_rsp = { 101 | 'rc': expected_rc, 102 | 'msg': ['OK'] 103 | } 104 | 105 | mock_zmq_context = mock_zmq_context.return_value 106 | mock_zmq_socket = mock_zmq_context.socket.return_value 107 | mock_zmq_socket.recv.return_value = json.dumps(srv_rsp).encode() 108 | mock_zmq_poller = mock_zmq_poller.return_value 109 | mock_zmq_poller.poll.return_value = { 110 | mock_zmq_socket: zmq.POLLIN 111 | } 112 | 113 | with mock.patch.object(sys, 'stdout', io.StringIO()) as output: 114 | 115 | rc = vbmc.main(['delete', 'foo', 'bar']) 116 | 117 | query = json.loads(mock_zmq_socket.send.call_args[0][0].decode()) 118 | 119 | expected_query = { 120 | "domain_names": ["foo", "bar"], 121 | "command": "delete", 122 | } 123 | 124 | self.assertEqual(expected_query, query) 125 | 126 | self.assertEqual(expected_rc, rc) 127 | self.assertEqual(expected_output, output.getvalue()) 128 | 129 | @mock.patch.object(zmq, 'Context') 130 | @mock.patch.object(zmq, 'Poller') 131 | def test_main_start(self, mock_zmq_poller, mock_zmq_context): 132 | expected_rc = 0 133 | expected_output = '' 134 | 135 | srv_rsp = { 136 | 'rc': expected_rc, 137 | 'msg': ['OK'] 138 | } 139 | 140 | mock_zmq_context = mock_zmq_context.return_value 141 | mock_zmq_socket = mock_zmq_context.socket.return_value 142 | mock_zmq_socket.recv.return_value = json.dumps(srv_rsp).encode() 143 | mock_zmq_poller = mock_zmq_poller.return_value 144 | mock_zmq_poller.poll.return_value = { 145 | mock_zmq_socket: zmq.POLLIN 146 | } 147 | 148 | with mock.patch.object(sys, 'stdout', io.StringIO()) as output: 149 | 150 | rc = vbmc.main(['start', 'foo', 'bar']) 151 | 152 | query = json.loads(mock_zmq_socket.send.call_args[0][0].decode()) 153 | 154 | expected_query = { 155 | 'command': 'start', 156 | 'domain_names': ['foo', 'bar'] 157 | } 158 | 159 | self.assertEqual(expected_query, query) 160 | 161 | self.assertEqual(expected_rc, rc) 162 | self.assertEqual(expected_output, output.getvalue()) 163 | 164 | @mock.patch.object(zmq, 'Context') 165 | @mock.patch.object(zmq, 'Poller') 166 | def test_main_stop(self, mock_zmq_poller, mock_zmq_context): 167 | expected_rc = 0 168 | expected_output = '' 169 | 170 | srv_rsp = { 171 | 'rc': expected_rc, 172 | 'msg': ['OK'] 173 | } 174 | 175 | mock_zmq_context = mock_zmq_context.return_value 176 | mock_zmq_socket = mock_zmq_context.socket.return_value 177 | mock_zmq_socket.recv.return_value = json.dumps(srv_rsp).encode() 178 | mock_zmq_poller = mock_zmq_poller.return_value 179 | mock_zmq_poller.poll.return_value = { 180 | mock_zmq_socket: zmq.POLLIN 181 | } 182 | 183 | with mock.patch.object(sys, 'stdout', io.StringIO()) as output: 184 | 185 | rc = vbmc.main(['stop', 'foo', 'bar']) 186 | 187 | query = json.loads(mock_zmq_socket.send.call_args[0][0].decode()) 188 | 189 | expected_query = { 190 | 'command': 'stop', 191 | 'domain_names': ['foo', 'bar'] 192 | } 193 | 194 | self.assertEqual(expected_query, query) 195 | 196 | self.assertEqual(expected_rc, rc) 197 | self.assertEqual(expected_output, output.getvalue()) 198 | 199 | @mock.patch.object(zmq, 'Context') 200 | @mock.patch.object(zmq, 'Poller') 201 | def test_main_list(self, mock_zmq_poller, mock_zmq_context): 202 | expected_rc = 0 203 | expected_output = """+-------+-------+ 204 | | col1 | col2 | 205 | +-------+-------+ 206 | | cell1 | cell2 | 207 | | cell3 | cell4 | 208 | +-------+-------+ 209 | """ 210 | 211 | srv_rsp = { 212 | 'rc': expected_rc, 213 | 'header': ['col1', 'col2'], 214 | 'rows': [['cell1', 'cell2'], 215 | ['cell3', 'cell4']], 216 | } 217 | 218 | mock_zmq_context = mock_zmq_context.return_value 219 | mock_zmq_socket = mock_zmq_context.socket.return_value 220 | mock_zmq_socket.recv.return_value = json.dumps(srv_rsp).encode() 221 | mock_zmq_poller = mock_zmq_poller.return_value 222 | mock_zmq_poller.poll.return_value = { 223 | mock_zmq_socket: zmq.POLLIN 224 | } 225 | 226 | with mock.patch.object(sys, 'stdout', io.StringIO()) as output: 227 | 228 | rc = vbmc.main(['list']) 229 | 230 | query = json.loads(mock_zmq_socket.send.call_args[0][0].decode()) 231 | 232 | expected_query = { 233 | "command": "list", 234 | } 235 | 236 | # Cliff adds some extra args to the query 237 | query = {key: query[key] for key in query 238 | if key in expected_query} 239 | 240 | self.assertEqual(expected_query, query) 241 | 242 | self.assertEqual(expected_rc, rc) 243 | self.assertEqual(expected_output, output.getvalue()) 244 | 245 | @mock.patch.object(zmq, 'Context') 246 | @mock.patch.object(zmq, 'Poller') 247 | def test_main_show(self, mock_zmq_poller, mock_zmq_context): 248 | expected_rc = 0 249 | 250 | expected_output = """+-------+-------+ 251 | | col1 | col2 | 252 | +-------+-------+ 253 | | cell1 | cell2 | 254 | | cell3 | cell4 | 255 | +-------+-------+ 256 | """ 257 | 258 | srv_rsp = { 259 | 'rc': expected_rc, 260 | 'header': ['col1', 'col2'], 261 | 'rows': [['cell1', 'cell2'], 262 | ['cell3', 'cell4']] 263 | } 264 | 265 | mock_zmq_context = mock_zmq_context.return_value 266 | mock_zmq_socket = mock_zmq_context.socket.return_value 267 | mock_zmq_socket.recv.return_value = json.dumps(srv_rsp).encode() 268 | mock_zmq_poller = mock_zmq_poller.return_value 269 | mock_zmq_poller.poll.return_value = { 270 | mock_zmq_socket: zmq.POLLIN 271 | } 272 | 273 | with mock.patch.object(sys, 'stdout', io.StringIO()) as output: 274 | 275 | rc = vbmc.main(['show', 'domain0']) 276 | 277 | query = json.loads(mock_zmq_socket.send.call_args[0][0].decode()) 278 | 279 | expected_query = { 280 | "domain_name": "domain0", 281 | "command": "show", 282 | } 283 | 284 | # Cliff adds some extra args to the query 285 | query = {key: query[key] for key in query 286 | if key in expected_query} 287 | 288 | self.assertEqual(expected_query, query) 289 | 290 | self.assertEqual(expected_rc, rc) 291 | self.assertEqual(expected_output, output.getvalue()) 292 | -------------------------------------------------------------------------------- /virtualbmc/tests/unit/cmd/test_vbmcd.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 builtins 17 | import os 18 | from unittest import mock 19 | 20 | 21 | from virtualbmc.cmd import vbmcd 22 | from virtualbmc import control 23 | from virtualbmc.tests.unit import base 24 | from virtualbmc import utils 25 | 26 | 27 | class VBMCDTestCase(base.TestCase): 28 | 29 | @mock.patch.object(builtins, 'open') 30 | @mock.patch.object(os, 'kill') 31 | @mock.patch.object(os, 'unlink') 32 | def test_main_foreground(self, mock_unlink, mock_kill, mock_open): 33 | with mock.patch.object(control, 'application') as mock_ml: 34 | mock_kill.side_effect = OSError() 35 | vbmcd.main(['--foreground']) 36 | mock_kill.assert_called_once() 37 | mock_ml.assert_called_once() 38 | mock_unlink.assert_called_once() 39 | 40 | @mock.patch.object(builtins, 'open') 41 | @mock.patch.object(os, 'kill') 42 | @mock.patch.object(os, 'unlink') 43 | def test_main_background(self, mock_unlink, mock_kill, mock_open): 44 | with mock.patch.object(utils, 'detach_process') as mock_dp: 45 | with mock.patch.object(control, 'application') as mock_ml: 46 | mock_kill.side_effect = OSError() 47 | mock_dp.return_value.__enter__.return_value = 0 48 | vbmcd.main([]) 49 | mock_kill.assert_called_once() 50 | mock_dp.assert_called_once() 51 | mock_ml.assert_called_once() 52 | mock_unlink.assert_called_once() 53 | -------------------------------------------------------------------------------- /virtualbmc/tests/unit/test_config.py: -------------------------------------------------------------------------------- 1 | # Copyright 2016 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 configparser 17 | import os 18 | from unittest import mock 19 | 20 | 21 | from virtualbmc import config 22 | from virtualbmc.tests.unit import base 23 | 24 | _CONFIG_FILE = '/foo/.vbmc/virtualbmc.conf' 25 | 26 | 27 | @mock.patch('virtualbmc.config.CONFIG_FILE', _CONFIG_FILE) 28 | class VirtualBMCConfigTestCase(base.TestCase): 29 | 30 | def setUp(self): 31 | super(VirtualBMCConfigTestCase, self).setUp() 32 | self.vbmc_config = config.VirtualBMCConfig() 33 | self.config_dict = {'default': {'show_passwords': 'true', 34 | 'config_dir': '/foo/bar/1', 35 | 'pid_file': '/foo/bar/2', 36 | 'server_port': '12345', 37 | 'server_spawn_wait': 3000, 38 | 'server_response_timeout': 5000}, 39 | 'log': {'debug': 'true', 'logfile': '/foo/bar/4'}, 40 | 'ipmi': {'session_timeout': '30'}} 41 | 42 | @mock.patch.object(config.VirtualBMCConfig, '_validate') 43 | @mock.patch.object(config.VirtualBMCConfig, '_as_dict') 44 | @mock.patch.object(configparser, 'ConfigParser') 45 | def test_initialize(self, mock_configparser, mock__as_dict, 46 | mock__validate): 47 | config = mock_configparser.return_value 48 | self.vbmc_config.initialize() 49 | 50 | config.read.assert_called_once_with(_CONFIG_FILE) 51 | mock__as_dict.assert_called_once_with(config) 52 | mock__validate.assert_called_once_with() 53 | 54 | @mock.patch.object(os.path, 'exists') 55 | def test__as_dict(self, mock_exists): 56 | mock_exists.side_effect = (False, True) 57 | config = mock.Mock() 58 | config.sections.side_effect = ['default', 'log', 'ipmi'], 59 | config.items.side_effect = [[('show_passwords', 'true'), 60 | ('config_dir', '/foo/bar/1'), 61 | ('pid_file', '/foo/bar/2'), 62 | ('server_port', '12345')], 63 | [('logfile', '/foo/bar/4'), 64 | ('debug', 'true')], 65 | [('session_timeout', '30')]] 66 | ret = self.vbmc_config._as_dict(config) 67 | self.assertEqual(self.config_dict, ret) 68 | 69 | def test_validate(self): 70 | self.vbmc_config._conf_dict = self.config_dict 71 | self.vbmc_config._validate() 72 | 73 | expected = self.config_dict.copy() 74 | expected['default']['show_passwords'] = True 75 | expected['default']['server_response_timeout'] = 5000 76 | expected['default']['server_spawn_wait'] = 3000 77 | expected['default']['server_port'] = 12345 78 | expected['log']['debug'] = True 79 | expected['ipmi']['session_timeout'] = 30 80 | self.assertEqual(expected, self.vbmc_config._conf_dict) 81 | -------------------------------------------------------------------------------- /virtualbmc/tests/unit/test_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 os 18 | from unittest import mock 19 | 20 | import zmq 21 | 22 | from virtualbmc import control 23 | from virtualbmc.tests.unit import base 24 | 25 | 26 | class VBMCControlServerTestCase(base.TestCase): 27 | 28 | @mock.patch.object(zmq, 'Context') 29 | @mock.patch.object(zmq, 'Poller') 30 | @mock.patch.object(os, 'path') 31 | @mock.patch.object(os, 'remove') 32 | def test_control_loop(self, mock_rm, mock_path, mock_zmq_poller, 33 | mock_zmq_context): 34 | mock_path.exists.return_value = False 35 | 36 | mock_vbmc_manager = mock.MagicMock() 37 | mock_handle_command = mock.MagicMock() 38 | 39 | req = { 40 | 'command': 'list', 41 | } 42 | 43 | mock_zmq_context = mock_zmq_context.return_value 44 | mock_zmq_socket = mock_zmq_context.socket.return_value 45 | mock_zmq_socket.recv.return_value = json.dumps(req).encode() 46 | mock_zmq_poller = mock_zmq_poller.return_value 47 | mock_zmq_poller.poll.return_value = { 48 | mock_zmq_socket: zmq.POLLIN 49 | } 50 | 51 | rsp = { 52 | 'rc': 0, 53 | 'msg': ['OK'] 54 | } 55 | 56 | class QuitNow(Exception): 57 | pass 58 | 59 | mock_handle_command.return_value = rsp 60 | mock_zmq_socket.send.side_effect = QuitNow() 61 | 62 | self.assertRaises(QuitNow, 63 | control.main_loop, 64 | mock_vbmc_manager, mock_handle_command) 65 | 66 | mock_zmq_socket.bind.assert_called_once() 67 | mock_handle_command.assert_called_once() 68 | 69 | response = json.loads(mock_zmq_socket.send.call_args[0][0].decode()) 70 | 71 | self.assertEqual(rsp, response) 72 | -------------------------------------------------------------------------------- /virtualbmc/tests/unit/test_manager.py: -------------------------------------------------------------------------------- 1 | # Copyright 2016 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 builtins 17 | import configparser 18 | import copy 19 | import errno 20 | import multiprocessing 21 | import os 22 | import shutil 23 | from unittest import mock 24 | 25 | 26 | from virtualbmc import exception 27 | from virtualbmc import manager 28 | from virtualbmc.tests.unit import base 29 | from virtualbmc.tests.unit import utils as test_utils 30 | from virtualbmc import utils 31 | 32 | _CONFIG_PATH = '/foo' 33 | 34 | 35 | class VirtualBMCManagerTestCase(base.TestCase): 36 | 37 | def setUp(self): 38 | super(VirtualBMCManagerTestCase, self).setUp() 39 | self.manager = manager.VirtualBMCManager() 40 | self.manager.config_dir = _CONFIG_PATH 41 | self.domain0 = test_utils.get_domain() 42 | self.domain1 = test_utils.get_domain(domain_name='Patrick', port=321) 43 | self.domain_name0 = self.domain0['domain_name'] 44 | self.domain_name1 = self.domain1['domain_name'] 45 | self.domain_path0 = os.path.join(_CONFIG_PATH, self.domain_name0) 46 | self.domain_path1 = os.path.join(_CONFIG_PATH, self.domain_name1) 47 | self.add_params = {'username': 'admin', 'password': 'pass', 48 | 'port': '777', 'address': '::', 49 | 'domain_name': 'Squidward Tentacles', 50 | 'libvirt_uri': 'foo://bar', 51 | 'libvirt_sasl_username': 'sasl_admin', 52 | 'libvirt_sasl_password': 'sasl_pass', 53 | 'active': 'False'} 54 | 55 | def _get_config(self, section, item): 56 | return self.domain0.get(item) 57 | 58 | @mock.patch.object(os.path, 'exists') 59 | @mock.patch.object(configparser, 'ConfigParser') 60 | def test__parse_config(self, mock_configparser, mock_exists): 61 | mock_exists.return_value = True 62 | config = mock_configparser.return_value 63 | config.get.side_effect = self._get_config 64 | config.getint.side_effect = self._get_config 65 | ret = self.manager._parse_config(self.domain_name0) 66 | 67 | self.assertEqual(self.domain0, ret) 68 | config.getint.assert_called_once_with('VirtualBMC', 'port') 69 | mock_configparser.assert_called_once_with() 70 | 71 | expected_get_calls = [mock.call('VirtualBMC', i) 72 | for i in ('username', 'password', 'address', 73 | 'port', 'domain_name', 'libvirt_uri', 74 | 'libvirt_sasl_username', 75 | 'libvirt_sasl_password', 76 | 'active')] 77 | self.assertEqual(expected_get_calls, config.get.call_args_list) 78 | 79 | @mock.patch.object(os.path, 'exists') 80 | def test__parse_config_domain_not_found(self, mock_exists): 81 | mock_exists.return_value = False 82 | self.assertRaises(exception.DomainNotFound, 83 | self.manager._parse_config, self.domain_name0) 84 | mock_exists.assert_called_once_with(self.domain_path0 + '/config') 85 | 86 | @mock.patch.object(builtins, 'open') 87 | @mock.patch.object(manager.VirtualBMCManager, '_parse_config') 88 | def _test__show(self, mock__parse, mock_open, expected=None): 89 | mock__parse.return_value = self.domain0 90 | f = mock.MagicMock() 91 | f.read.return_value = self.domain0['port'] 92 | mock_open.return_value.__enter__.return_value = f 93 | 94 | if expected is None: 95 | expected = self.domain0.copy() 96 | expected['status'] = manager.DOWN 97 | 98 | ret = self.manager._show(self.domain_name0) 99 | self.assertEqual(expected, ret) 100 | 101 | def test__show(self): 102 | conf = {'default': {'show_passwords': True}} 103 | with mock.patch('virtualbmc.manager.CONF', conf): 104 | self._test__show() 105 | 106 | def test__show_mask_passwords(self): 107 | conf = {'default': {'show_passwords': False}} 108 | with mock.patch('virtualbmc.manager.CONF', conf): 109 | expected = self.domain0.copy() 110 | expected['password'] = '***' 111 | expected['libvirt_sasl_password'] = '***' 112 | expected['status'] = manager.DOWN 113 | self._test__show(expected=expected) 114 | 115 | @mock.patch.object(builtins, 'open') 116 | @mock.patch.object(configparser, 'ConfigParser') 117 | @mock.patch.object(os, 'makedirs') 118 | @mock.patch.object(utils, 'check_libvirt_connection_and_domain') 119 | def test_add(self, mock_check_conn, mock_makedirs, mock_configparser, 120 | mock_open): 121 | config = mock_configparser.return_value 122 | params = copy.copy(self.add_params) 123 | self.manager.add(**params) 124 | 125 | expected_calls = [mock.call('VirtualBMC', i, self.add_params[i]) 126 | for i in self.add_params] 127 | self.assertEqual(sorted(expected_calls), 128 | sorted(config.set.call_args_list)) 129 | config.add_section.assert_called_once_with('VirtualBMC') 130 | config.write.assert_called_once_with(mock.ANY) 131 | mock_check_conn.assert_called_once_with( 132 | self.add_params['libvirt_uri'], self.add_params['domain_name'], 133 | sasl_username=self.add_params['libvirt_sasl_username'], 134 | sasl_password=self.add_params['libvirt_sasl_password']) 135 | mock_makedirs.assert_called_once_with( 136 | os.path.join(_CONFIG_PATH, self.add_params['domain_name'])) 137 | mock_configparser.assert_called_once_with() 138 | 139 | @mock.patch.object(builtins, 'open') 140 | @mock.patch.object(configparser, 'ConfigParser') 141 | @mock.patch.object(os, 'makedirs') 142 | @mock.patch.object(utils, 'check_libvirt_connection_and_domain') 143 | def test_add_with_port_as_int(self, mock_check_conn, mock_makedirs, 144 | mock_configparser, mock_open): 145 | config = mock_configparser.return_value 146 | params = copy.copy(self.add_params) 147 | params['port'] = int(params['port']) 148 | self.manager.add(**params) 149 | 150 | expected_calls = [mock.call('VirtualBMC', i, self.add_params[i]) 151 | for i in self.add_params] 152 | self.assertEqual(sorted(expected_calls), 153 | sorted(config.set.call_args_list)) 154 | config.add_section.assert_called_once_with('VirtualBMC') 155 | config.write.assert_called_once_with(mock.ANY) 156 | mock_check_conn.assert_called_once_with( 157 | self.add_params['libvirt_uri'], self.add_params['domain_name'], 158 | sasl_username=self.add_params['libvirt_sasl_username'], 159 | sasl_password=self.add_params['libvirt_sasl_password']) 160 | mock_makedirs.assert_called_once_with( 161 | os.path.join(_CONFIG_PATH, self.add_params['domain_name'])) 162 | mock_configparser.assert_called_once_with() 163 | 164 | @mock.patch.object(os, 'makedirs') 165 | @mock.patch.object(utils, 'check_libvirt_connection_and_domain') 166 | def test_add_domain_already_exist(self, mock_check_conn, mock_makedirs): 167 | os_error = OSError() 168 | os_error.errno = errno.EEXIST 169 | mock_makedirs.side_effect = os_error 170 | 171 | ret, _ = self.manager.add(**self.add_params) 172 | 173 | expected_ret = 1 174 | 175 | self.assertEqual(ret, expected_ret) 176 | 177 | mock_check_conn.assert_called_once_with( 178 | self.add_params['libvirt_uri'], self.add_params['domain_name'], 179 | sasl_username=self.add_params['libvirt_sasl_username'], 180 | sasl_password=self.add_params['libvirt_sasl_password']) 181 | 182 | @mock.patch.object(os, 'makedirs') 183 | @mock.patch.object(utils, 'check_libvirt_connection_and_domain') 184 | def test_add_oserror(self, mock_check_conn, mock_makedirs): 185 | mock_makedirs.side_effect = OSError 186 | 187 | ret, _ = self.manager.add(**self.add_params) 188 | expected_ret = 1 189 | self.assertEqual(ret, expected_ret) 190 | 191 | mock_check_conn.assert_called_once_with( 192 | self.add_params['libvirt_uri'], self.add_params['domain_name'], 193 | sasl_username=self.add_params['libvirt_sasl_username'], 194 | sasl_password=self.add_params['libvirt_sasl_password']) 195 | 196 | @mock.patch.object(shutil, 'rmtree') 197 | @mock.patch.object(os.path, 'exists') 198 | @mock.patch.object(manager.VirtualBMCManager, 'stop') 199 | def test_delete(self, mock_stop, mock_exists, mock_rmtree): 200 | mock_exists.return_value = True 201 | self.manager.delete(self.domain_name0) 202 | 203 | mock_exists.assert_called_once_with(self.domain_path0) 204 | mock_stop.assert_called_once_with(self.domain_name0) 205 | mock_rmtree.assert_called_once_with(self.domain_path0) 206 | 207 | @mock.patch.object(os.path, 'exists') 208 | def test_delete_domain_not_found(self, mock_exists): 209 | mock_exists.return_value = False 210 | self.assertRaises(exception.DomainNotFound, 211 | self.manager.delete, self.domain_name0) 212 | mock_exists.assert_called_once_with(self.domain_path0) 213 | 214 | @mock.patch.object(builtins, 'open') 215 | @mock.patch.object(manager.VirtualBMCManager, '_parse_config') 216 | @mock.patch.object(os.path, 'exists') 217 | @mock.patch.object(os.path, 'isdir') 218 | @mock.patch.object(os, 'listdir') 219 | @mock.patch.object(multiprocessing, 'Process') 220 | def test_start(self, mock_process, mock_listdir, mock_isdir, mock_exists, 221 | mock__parse, mock_open): 222 | conf = {'ipmi': {'session_timeout': 10}, 223 | 'default': {'show_passwords': False}} 224 | with mock.patch('virtualbmc.manager.CONF', conf): 225 | mock_listdir.return_value = [self.domain_name0] 226 | mock_isdir.return_value = True 227 | mock_exists.return_value = True 228 | domain0_conf = self.domain0.copy() 229 | domain0_conf.update(active='False') 230 | mock__parse.return_value = domain0_conf 231 | file_handler = mock_open.return_value.__enter__.return_value 232 | self.manager.start(self.domain_name0) 233 | mock__parse.assert_called_with(self.domain_name0) 234 | self.assertEqual(file_handler.write.call_count, 9) 235 | 236 | @mock.patch.object(builtins, 'open') 237 | @mock.patch.object(manager.VirtualBMCManager, '_parse_config') 238 | @mock.patch.object(os.path, 'isdir') 239 | @mock.patch.object(os, 'listdir') 240 | def test_stop(self, mock_listdir, mock_isdir, mock__parse, mock_open): 241 | conf = {'ipmi': {'session_timeout': 10}, 242 | 'default': {'show_passwords': False}} 243 | with mock.patch('virtualbmc.manager.CONF', conf): 244 | mock_listdir.return_value = [self.domain_name0] 245 | mock_isdir.return_value = True 246 | domain0_conf = self.domain0.copy() 247 | domain0_conf.update(active='True') 248 | mock__parse.return_value = domain0_conf 249 | file_handler = mock_open.return_value.__enter__.return_value 250 | self.manager.stop(self.domain_name0) 251 | mock_isdir.assert_called_once_with(self.domain_path0) 252 | mock__parse.assert_called_with(self.domain_name0) 253 | self.assertEqual(file_handler.write.call_count, 9) 254 | 255 | @mock.patch.object(os.path, 'exists') 256 | def test_stop_domain_not_found(self, mock_exists): 257 | mock_exists.return_value = False 258 | ret = self.manager.stop(self.domain_name0) 259 | expected_ret = 1, 'No domain with matching name SpongeBob was found' 260 | self.assertEqual(ret, expected_ret) 261 | mock_exists.assert_called_once_with( 262 | os.path.join(self.domain_path0, 'config') 263 | ) 264 | 265 | @mock.patch.object(os.path, 'isdir') 266 | @mock.patch.object(os, 'listdir') 267 | @mock.patch.object(manager.VirtualBMCManager, '_show') 268 | def test_list(self, mock__show, mock_listdir, mock_isdir): 269 | mock_isdir.return_value = True 270 | mock_listdir.return_value = (self.domain_name0, self.domain_name1) 271 | 272 | ret, _ = self.manager.list() 273 | expected_ret = 0 274 | self.assertEqual(ret, expected_ret) 275 | mock_listdir.assert_called_once_with(_CONFIG_PATH) 276 | expected_calls = [mock.call(self.domain_path0), 277 | mock.call(self.domain_path1)] 278 | self.assertEqual(expected_calls, mock_isdir.call_args_list) 279 | expected_calls = [mock.call(self.domain_name0), 280 | mock.call(self.domain_name1)] 281 | self.assertEqual(expected_calls, mock__show.call_args_list) 282 | 283 | @mock.patch.object(manager.VirtualBMCManager, '_show') 284 | def test_show(self, mock__show): 285 | self.manager.show(self.domain0) 286 | mock__show.assert_called_once_with(self.domain0) 287 | -------------------------------------------------------------------------------- /virtualbmc/tests/unit/test_utils.py: -------------------------------------------------------------------------------- 1 | # Copyright 2016 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 os 17 | from unittest import mock 18 | 19 | import libvirt 20 | 21 | from virtualbmc import exception 22 | from virtualbmc.tests.unit import base 23 | from virtualbmc import utils 24 | 25 | 26 | class MiscUtilsTestCase(base.TestCase): 27 | 28 | @mock.patch.object(os, 'kill') 29 | def test_is_pid_running(self, mock_kill): 30 | self.assertTrue(utils.is_pid_running(123)) 31 | mock_kill.assert_called_once_with(123, 0) 32 | 33 | @mock.patch.object(os, 'kill') 34 | def test_is_pid_running_not_running(self, mock_kill): 35 | mock_kill.side_effect = OSError('boom') 36 | self.assertFalse(utils.is_pid_running(123)) 37 | mock_kill.assert_called_once_with(123, 0) 38 | 39 | def test_str2bool(self): 40 | for b in ('TRUE', 'true', 'True'): 41 | self.assertTrue(utils.str2bool(b)) 42 | 43 | for b in ('FALSE', 'false', 'False'): 44 | self.assertFalse(utils.str2bool(b)) 45 | 46 | self.assertRaises(ValueError, utils.str2bool, 'bogus value') 47 | 48 | def test_mask_dict_password(self): 49 | input_dict = {'foo': 'bar', 'password': 'SpongeBob SquarePants'} 50 | output_dict = utils.mask_dict_password(input_dict) 51 | expected = {'foo': 'bar', 'password': '***'} 52 | self.assertEqual(expected, output_dict) 53 | 54 | 55 | class LibvirtUtilsTestCase(base.TestCase): 56 | 57 | def setUp(self): 58 | super(LibvirtUtilsTestCase, self).setUp() 59 | self.fake_connection = mock.Mock() 60 | self.uri = 'fake:///patrick' 61 | 62 | def test_get_libvirt_domain(self): 63 | self.fake_connection.lookupByName.return_value = 'fake connection' 64 | ret = utils.get_libvirt_domain(self.fake_connection, 'SpongeBob') 65 | 66 | self.fake_connection.lookupByName.assert_called_once_with('SpongeBob') 67 | self.assertEqual('fake connection', ret) 68 | 69 | def test_get_libvirt_domain_not_found(self): 70 | self.fake_connection.lookupByName.side_effect = libvirt.libvirtError( 71 | 'boom') 72 | self.assertRaises(exception.DomainNotFound, utils.get_libvirt_domain, 73 | self.fake_connection, 'Fred') 74 | self.fake_connection.lookupByName.assert_called_once_with('Fred') 75 | 76 | def _test_libvirt_open(self, mock_open, **kwargs): 77 | mock_open.return_value = self.fake_connection 78 | with utils.libvirt_open(self.uri, **kwargs) as conn: 79 | self.assertEqual(self.fake_connection, conn) 80 | 81 | self.fake_connection.close.assert_called_once_with() 82 | 83 | @mock.patch.object(libvirt, 'open') 84 | def test_libvirt_open(self, mock_open): 85 | self._test_libvirt_open(mock_open) 86 | mock_open.assert_called_once_with(self.uri) 87 | 88 | @mock.patch.object(libvirt, 'open') 89 | def test_libvirt_open_error(self, mock_open): 90 | mock_open.side_effect = libvirt.libvirtError('boom') 91 | self.assertRaises(exception.LibvirtConnectionOpenError, 92 | self._test_libvirt_open, mock_open) 93 | mock_open.assert_called_once_with(self.uri) 94 | 95 | @mock.patch.object(libvirt, 'openReadOnly') 96 | def test_libvirt_open_readonly(self, mock_open): 97 | self._test_libvirt_open(mock_open, readonly=True) 98 | mock_open.assert_called_once_with(self.uri) 99 | 100 | @mock.patch.object(libvirt, 'openAuth') 101 | def _test_libvirt_open_sasl(self, mock_open, readonly=False): 102 | username = 'Eugene H. Krabs' 103 | password = ('hamburger, fresh lettuce, crisp onions, tomatoes, ' 104 | 'undersea cheese, pickles, mustard and ketchup') 105 | self._test_libvirt_open(mock_open, sasl_username=username, 106 | sasl_password=password, readonly=readonly) 107 | ro = 1 if readonly else 0 108 | mock_open.assert_called_once_with(self.uri, mock.ANY, ro) 109 | 110 | def test_libvirt_open_sasl(self): 111 | self._test_libvirt_open_sasl() 112 | 113 | def test_libvirt_open_sasl_readonly(self): 114 | self._test_libvirt_open_sasl(readonly=True) 115 | 116 | 117 | @mock.patch.object(utils, 'os') 118 | class DetachProcessUtilsTestCase(base.TestCase): 119 | 120 | def test_detach_process(self, mock_os): 121 | 122 | # 2nd value > 0 so _exit get called and we can assert that we've 123 | # killed the parent's process 124 | mock_os.fork.side_effect = (0, 999) 125 | mock_os.devnull = os.devnull 126 | 127 | with utils.detach_process() as pid: 128 | self.assertEqual(0, pid) 129 | 130 | # assert fork() has been called twice 131 | expected_fork_calls = [mock.call()] * 2 132 | self.assertEqual(expected_fork_calls, mock_os.fork.call_args_list) 133 | 134 | mock_os.setsid.assert_called_once_with() 135 | mock_os.chdir.assert_called_once_with('/') 136 | mock_os.umask.assert_called_once_with(0) 137 | mock_os._exit.assert_called_once_with(0) 138 | 139 | def test_detach_process_fork_fail(self, mock_os): 140 | error_msg = 'Kare-a-tay!' 141 | mock_os.fork.side_effect = OSError(error_msg) 142 | 143 | with self.assertRaisesRegex(exception.DetachProcessError, error_msg): 144 | with utils.detach_process(): 145 | pass 146 | 147 | mock_os.fork.assert_called_once_with() 148 | self.assertFalse(mock_os.setsid.called) 149 | self.assertFalse(mock_os.chdir.called) 150 | self.assertFalse(mock_os.umask.called) 151 | self.assertFalse(mock_os._exit.called) 152 | 153 | def test_detach_process_chdir_fail(self, mock_os): 154 | # 2nd value > 0 so _exit get called and we can assert that we've 155 | # killed the parent's process 156 | mock_os.fork.side_effect = (0, 999) 157 | 158 | error_msg = 'Fish paste!' 159 | mock_os.chdir.side_effect = Exception(error_msg) 160 | 161 | with self.assertRaisesRegex(exception.DetachProcessError, error_msg): 162 | with utils.detach_process(): 163 | pass 164 | 165 | # assert fork() has been called twice 166 | expected_fork_calls = [mock.call()] * 2 167 | self.assertEqual(expected_fork_calls, mock_os.fork.call_args_list) 168 | 169 | mock_os.setsid.assert_called_once_with() 170 | mock_os.chdir.assert_called_once_with('/') 171 | mock_os._exit.assert_called_once_with(0) 172 | self.assertFalse(mock_os.umask.called) 173 | 174 | def test_detach_process_umask_fail(self, mock_os): 175 | # 2nd value > 0 so _exit get called and we can assert that we've 176 | # killed the parent's process 177 | mock_os.fork.side_effect = (0, 999) 178 | 179 | error_msg = 'Barnacles!' 180 | mock_os.umask.side_effect = Exception(error_msg) 181 | 182 | with self.assertRaisesRegex(exception.DetachProcessError, error_msg): 183 | with utils.detach_process(): 184 | pass 185 | 186 | # assert fork() has been called twice 187 | expected_fork_calls = [mock.call()] * 2 188 | self.assertEqual(expected_fork_calls, mock_os.fork.call_args_list) 189 | 190 | mock_os.setsid.assert_called_once_with() 191 | mock_os.chdir.assert_called_once_with('/') 192 | mock_os._exit.assert_called_once_with(0) 193 | mock_os.umask.assert_called_once_with(0) 194 | -------------------------------------------------------------------------------- /virtualbmc/tests/unit/test_vbmc.py: -------------------------------------------------------------------------------- 1 | # Copyright 2016 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 | from unittest import mock 17 | 18 | import libvirt 19 | 20 | from virtualbmc import exception 21 | from virtualbmc.tests.unit import base 22 | from virtualbmc.tests.unit import utils as test_utils 23 | from virtualbmc import utils 24 | from virtualbmc import vbmc 25 | 26 | DOMAIN_XML_TEMPLATE = """\ 27 | 28 | 29 | hvm 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | """ 44 | 45 | 46 | @mock.patch.object(utils, 'libvirt_open') 47 | @mock.patch.object(utils, 'get_libvirt_domain') 48 | class VirtualBMCTestCase(base.TestCase): 49 | 50 | def setUp(self): 51 | super(VirtualBMCTestCase, self).setUp() 52 | self.domain = test_utils.get_domain() 53 | # NOTE(lucasagomes): pyghmi's Bmc does create a socket in the 54 | # constructor so we need to mock it here 55 | mock.patch('pyghmi.ipmi.bmc.Bmc.__init__', 56 | lambda *args, **kwargs: None).start() 57 | self.vbmc = vbmc.VirtualBMC(**self.domain) 58 | 59 | def _assert_libvirt_calls(self, mock_libvirt_domain, mock_libvirt_open, 60 | readonly=False): 61 | """Helper method to assert that the LibVirt calls were invoked.""" 62 | mock_libvirt_domain.assert_called_once_with( 63 | mock.ANY, self.domain['domain_name']) 64 | params = {'sasl_password': self.domain['libvirt_sasl_password'], 65 | 'sasl_username': self.domain['libvirt_sasl_username'], 66 | 'uri': self.domain['libvirt_uri']} 67 | if readonly: 68 | params['readonly'] = True 69 | mock_libvirt_open.assert_called_once_with(**params) 70 | 71 | def test_get_boot_device(self, mock_libvirt_domain, mock_libvirt_open): 72 | for boot_device in vbmc.GET_BOOT_DEVICES_MAP: 73 | domain_xml = DOMAIN_XML_TEMPLATE % boot_device 74 | mock_libvirt_domain.return_value.XMLDesc.return_value = domain_xml 75 | ret = self.vbmc.get_boot_device() 76 | 77 | self.assertEqual(vbmc.GET_BOOT_DEVICES_MAP[boot_device], ret) 78 | self._assert_libvirt_calls(mock_libvirt_domain, mock_libvirt_open, 79 | readonly=True) 80 | 81 | # reset mocks for the next iteration 82 | mock_libvirt_domain.reset_mock() 83 | mock_libvirt_open.reset_mock() 84 | 85 | def test_set_boot_device(self, mock_libvirt_domain, mock_libvirt_open): 86 | for boot_device in vbmc.SET_BOOT_DEVICES_MAP: 87 | domain_xml = DOMAIN_XML_TEMPLATE % 'foo' 88 | mock_libvirt_domain.return_value.XMLDesc.return_value = domain_xml 89 | conn = mock_libvirt_open.return_value.__enter__.return_value 90 | self.vbmc.set_boot_device(boot_device) 91 | 92 | expected = ('' % 93 | vbmc.SET_BOOT_DEVICES_MAP[boot_device]) 94 | self.assertIn(expected, str(conn.defineXML.call_args)) 95 | self.assertEqual(1, str(conn.defineXML.call_args).count(' 0 and parent_exits: 106 | os._exit(0) 107 | 108 | return pid 109 | 110 | except OSError as e: 111 | raise exception.DetachProcessError(error=e) 112 | 113 | def _change_root_directory(self): 114 | """Change to root directory. 115 | 116 | Ensure that our process doesn't keep any directory in use. Failure 117 | to do this could make it so that an administrator couldn't 118 | unmount a filesystem, because it was our current directory. 119 | """ 120 | try: 121 | os.chdir('/') 122 | except Exception as e: 123 | error = ('Failed to change root directory. Error: %s' % e) 124 | raise exception.DetachProcessError(error=error) 125 | 126 | def _change_file_creation_mask(self): 127 | """Set the umask for new files. 128 | 129 | Set the umask for new files the process creates so that it does 130 | have complete control over the permissions of them. We don't 131 | know what umask we may have inherited. 132 | """ 133 | try: 134 | os.umask(0) 135 | except Exception as e: 136 | error = ('Failed to change file creation mask. Error: %s' % e) 137 | raise exception.DetachProcessError(error=error) 138 | 139 | def __enter__(self): 140 | pid = self._fork(parent_exits=False) 141 | if pid > 0: 142 | return pid 143 | 144 | os.setsid() 145 | 146 | self._fork(parent_exits=True) 147 | 148 | self._change_root_directory() 149 | self._change_file_creation_mask() 150 | 151 | sys.stdout.flush() 152 | sys.stderr.flush() 153 | 154 | si = open(os.devnull, 'r') 155 | so = open(os.devnull, 'a+') 156 | se = open(os.devnull, 'a+') 157 | 158 | os.dup2(si.fileno(), sys.stdin.fileno()) 159 | os.dup2(so.fileno(), sys.stdout.fileno()) 160 | os.dup2(se.fileno(), sys.stderr.fileno()) 161 | 162 | return pid 163 | 164 | def __exit__(self, type, value, traceback): 165 | pass 166 | -------------------------------------------------------------------------------- /virtualbmc/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 libvirt 16 | import pyghmi.ipmi.bmc as bmc 17 | 18 | from virtualbmc import exception 19 | from virtualbmc import log 20 | from virtualbmc import utils 21 | 22 | LOG = log.get_logger() 23 | 24 | # Power states 25 | POWEROFF = 0 26 | POWERON = 1 27 | 28 | # From the IPMI - Intelligent Platform Management Interface Specification 29 | # Second Generation v2.0 Document Revision 1.1 October 1, 2013 30 | # https://www.intel.com/content/dam/www/public/us/en/documents/product-briefs/ipmi-second-gen-interface-spec-v2-rev1-1.pdf 31 | # 32 | # Command failed and can be retried 33 | IPMI_COMMAND_NODE_BUSY = 0xC0 34 | # Invalid data field in request 35 | IPMI_INVALID_DATA = 0xcc 36 | 37 | # Boot device maps 38 | GET_BOOT_DEVICES_MAP = { 39 | 'network': 4, 40 | 'hd': 8, 41 | 'cdrom': 0x14, 42 | } 43 | 44 | SET_BOOT_DEVICES_MAP = { 45 | 'network': 'network', 46 | 'hd': 'hd', 47 | 'optical': 'cdrom', 48 | } 49 | 50 | 51 | class VirtualBMC(bmc.Bmc): 52 | 53 | def __init__(self, username, password, port, address, 54 | domain_name, libvirt_uri, libvirt_sasl_username=None, 55 | libvirt_sasl_password=None, **kwargs): 56 | super(VirtualBMC, self).__init__({username: password}, 57 | port=port, address=address) 58 | self.domain_name = domain_name 59 | self._conn_args = {'uri': libvirt_uri, 60 | 'sasl_username': libvirt_sasl_username, 61 | 'sasl_password': libvirt_sasl_password} 62 | 63 | # Copied from nova/virt/libvirt/guest.py 64 | def get_xml_desc(self, domain, dump_sensitive=False): 65 | """Returns xml description of guest. 66 | 67 | :param domain: The libvirt domain to call 68 | :param dump_sensitive: Dump security sensitive information 69 | :returns string: XML description of the guest 70 | """ 71 | flags = dump_sensitive and libvirt.VIR_DOMAIN_XML_SECURE or 0 72 | return domain.XMLDesc(flags=flags) 73 | 74 | def get_boot_device(self): 75 | LOG.debug('Get boot device called for %(domain)s', 76 | {'domain': self.domain_name}) 77 | with utils.libvirt_open(readonly=True, **self._conn_args) as conn: 78 | domain = utils.get_libvirt_domain(conn, self.domain_name) 79 | boot_element = ET.fromstring(domain.XMLDesc()).find('.//os/boot') 80 | boot_dev = None 81 | if boot_element is not None: 82 | boot_dev = boot_element.attrib.get('dev') 83 | return GET_BOOT_DEVICES_MAP.get(boot_dev, 0) 84 | 85 | def _remove_boot_elements(self, parent_element): 86 | for boot_element in parent_element.findall('boot'): 87 | parent_element.remove(boot_element) 88 | 89 | def set_boot_device(self, bootdevice): 90 | LOG.debug('Set boot device called for %(domain)s with boot ' 91 | 'device "%(bootdev)s"', {'domain': self.domain_name, 92 | 'bootdev': bootdevice}) 93 | device = SET_BOOT_DEVICES_MAP.get(bootdevice) 94 | if device is None: 95 | # Invalid data field in request 96 | return IPMI_INVALID_DATA 97 | 98 | try: 99 | with utils.libvirt_open(**self._conn_args) as conn: 100 | domain = utils.get_libvirt_domain(conn, self.domain_name) 101 | tree = ET.fromstring( 102 | self.get_xml_desc(domain, dump_sensitive=True)) 103 | 104 | # Remove all "boot" element under "devices" 105 | # They are mutually exclusive with "os/boot" 106 | for device_element in tree.findall('devices/*'): 107 | self._remove_boot_elements(device_element) 108 | 109 | for os_element in tree.findall('os'): 110 | # Remove all "boot" elements under "os" 111 | self._remove_boot_elements(os_element) 112 | 113 | # Add a new boot element with the request boot device 114 | boot_element = ET.SubElement(os_element, 'boot') 115 | boot_element.set('dev', device) 116 | 117 | conn.defineXML(ET.tostring(tree, encoding="unicode")) 118 | except Exception: 119 | LOG.error('Failed setting the boot device %(bootdev)s for ' 120 | 'domain %(domain)s', {'bootdev': device, 121 | 'domain': self.domain_name}) 122 | # Command failed, but let client to retry 123 | return IPMI_COMMAND_NODE_BUSY 124 | 125 | def get_power_state(self): 126 | LOG.debug('Get power state called for domain %(domain)s', 127 | {'domain': self.domain_name}) 128 | try: 129 | with utils.libvirt_open(readonly=True, **self._conn_args) as conn: 130 | domain = utils.get_libvirt_domain(conn, self.domain_name) 131 | if domain.isActive(): 132 | return POWERON 133 | except Exception as e: 134 | msg = ('Error getting the power state of domain %(domain)s. ' 135 | 'Error: %(error)s' % {'domain': self.domain_name, 136 | 'error': e}) 137 | LOG.error(msg) 138 | raise exception.VirtualBMCError(message=msg) 139 | 140 | return POWEROFF 141 | 142 | def pulse_diag(self): 143 | LOG.debug('Power diag called for domain %(domain)s', 144 | {'domain': self.domain_name}) 145 | try: 146 | with utils.libvirt_open(**self._conn_args) as conn: 147 | domain = utils.get_libvirt_domain(conn, self.domain_name) 148 | if domain.isActive(): 149 | domain.injectNMI() 150 | except Exception as e: 151 | LOG.error('Error powering diag the domain %(domain)s. ' 152 | 'Error: %(error)s', {'domain': self.domain_name, 153 | 'error': e}) 154 | # Command failed, but let client to retry 155 | return IPMI_COMMAND_NODE_BUSY 156 | 157 | def power_off(self): 158 | LOG.debug('Power off called for domain %(domain)s', 159 | {'domain': self.domain_name}) 160 | try: 161 | with utils.libvirt_open(**self._conn_args) as conn: 162 | domain = utils.get_libvirt_domain(conn, self.domain_name) 163 | if domain.isActive(): 164 | domain.destroy() 165 | except Exception as e: 166 | LOG.error('Error powering off the domain %(domain)s. ' 167 | 'Error: %(error)s', {'domain': self.domain_name, 168 | 'error': e}) 169 | # Command failed, but let client to retry 170 | return IPMI_COMMAND_NODE_BUSY 171 | 172 | def power_on(self): 173 | LOG.debug('Power on called for domain %(domain)s', 174 | {'domain': self.domain_name}) 175 | try: 176 | with utils.libvirt_open(**self._conn_args) as conn: 177 | domain = utils.get_libvirt_domain(conn, self.domain_name) 178 | if not domain.isActive(): 179 | domain.create() 180 | except Exception as e: 181 | LOG.error('Error powering on the domain %(domain)s. ' 182 | 'Error: %(error)s', {'domain': self.domain_name, 183 | 'error': e}) 184 | # Command failed, but let client to retry 185 | return IPMI_COMMAND_NODE_BUSY 186 | 187 | def power_shutdown(self): 188 | LOG.debug('Soft power off called for domain %(domain)s', 189 | {'domain': self.domain_name}) 190 | try: 191 | with utils.libvirt_open(**self._conn_args) as conn: 192 | domain = utils.get_libvirt_domain(conn, self.domain_name) 193 | if domain.isActive(): 194 | domain.shutdown() 195 | except Exception as e: 196 | LOG.error('Error soft powering off the domain %(domain)s. ' 197 | 'Error: %(error)s', {'domain': self.domain_name, 198 | 'error': e}) 199 | # Command failed, but let client to retry 200 | return IPMI_COMMAND_NODE_BUSY 201 | 202 | def power_reset(self): 203 | LOG.debug('Power reset called for domain %(domain)s', 204 | {'domain': self.domain_name}) 205 | try: 206 | with utils.libvirt_open(**self._conn_args) as conn: 207 | domain = utils.get_libvirt_domain(conn, self.domain_name) 208 | if domain.isActive(): 209 | domain.reset() 210 | except Exception as e: 211 | LOG.error('Error resetting the domain %(domain)s. ' 212 | 'Error: %(error)s', {'domain': self.domain_name, 213 | 'error': e}) 214 | # Command not supported in present state 215 | return IPMI_COMMAND_NODE_BUSY 216 | -------------------------------------------------------------------------------- /zuul.d/project.yaml: -------------------------------------------------------------------------------- 1 | - project: 2 | templates: 3 | - check-requirements 4 | - openstack-cover-jobs 5 | - openstack-python3-jobs 6 | - publish-openstack-docs-pti 7 | - release-notes-jobs-python3 8 | check: 9 | jobs: 10 | - virtualbmc-tempest-ironic 11 | - virtualbmc-tox-codespell: 12 | voting: false 13 | gate: 14 | jobs: 15 | - virtualbmc-tempest-ironic 16 | -------------------------------------------------------------------------------- /zuul.d/virtualbmc-jobs.yaml: -------------------------------------------------------------------------------- 1 | - job: 2 | name: virtualbmc-tempest-ironic 3 | parent: ironic-base 4 | irrelevant-files: 5 | - ^.*\.rst$ 6 | - ^doc/.*$ 7 | - ^virtualbmc/tests/.*$ 8 | - ^setup.cfg$ 9 | - ^test-requirements.txt$ 10 | - ^tools/.*$ 11 | - ^tox.ini$ 12 | timeout: 10800 13 | required-projects: 14 | - openstack/virtualbmc 15 | vars: 16 | devstack_localrc: 17 | EBTABLES_RACE_FIX: True 18 | IRONIC_BOOT_MODE: bios 19 | IRONIC_DEFAULT_BOOT_OPTION: netboot 20 | IRONIC_DEFAULT_RESCUE_INTERFACE: "" 21 | devstack_services: 22 | dstat: false 23 | 24 | # Remove when no longer used on pyghmi 25 | - job: 26 | name: virtualbmc-tempest-ironic-ipmi-iscsi 27 | parent: virtualbmc-tempest-ironic 28 | 29 | - job: 30 | name: virtualbmc-tox-codespell 31 | parent: openstack-tox 32 | timeout: 7200 33 | vars: 34 | tox_envlist: codespell --------------------------------------------------------------------------------