├── .gitignore ├── LICENSE ├── README.md ├── proxmoxbmc ├── __init__.py ├── cmd │ ├── __init__.py │ ├── pbmc.py │ └── pbmcd.py ├── config.py ├── control.py ├── exception.py ├── log.py ├── manager.py ├── pbmc.py └── utils.py ├── requirements.txt ├── setup.cfg └── setup.py /.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 | .vscode/ 61 | 62 | # Environments 63 | .env/ 64 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # ProxmoxBMC 2 | Based on VirtualBMC @ https://github.com/openstack/virtualbmc 3 | 4 | Adapted to make it use the proxmoxer (https://github.com/proxmoxer/proxmoxer) package against the Proxmox VE API instead of libvirt. 5 | 6 | ## Usage 7 | On a debian based distribution: 8 | Make sure you have python3-pip and python3-venv installed 9 | ``` 10 | apt-get update && apt-get install python3-pip python3-venv 11 | git clone https://github.com/agnon/proxmoxbmc.git 12 | cd proxmoxbmc 13 | python3 -m venv .env 14 | . .env/bin/activate 15 | pip install -r requirements.txt 16 | python -m setup install 17 | pbmcd # starts the server 18 | # Add a VM 19 | # username = the username used for logging in to the emulated BMC 20 | # password = the password used for logging in to the emulated BMC 21 | # port = the port for the emulated BMC. Specify this if you emulate multiple BMCs on this server 22 | # address = the address to bind to. Binds to 0.0.0.0 by default 23 | # proxmox-address = The address to a proxmox node, prefferably a VIP of the cluster 24 | # token-user = the user that the token belongs to like root@pam 25 | # token-name = the name of the token, for instance ipmi or bmc 26 | # token-value = the actual value of the token 27 | # Example of adding the VMID 123 on port 6625 with admin/password as login using a token for the root user named ipmi 28 | pbmc add --username admin --password password --port 6625 --proxmox-address proxmox.example.org --token-user root@pam --token-name ipmi --token-value {token} 123 29 | # If all went well you should now see it in the list of BMCs 30 | pbmc list 31 | # Now start it 32 | pbmc start 123 33 | ``` 34 | 35 | ## Start as a service (for systemd) 36 | Put this content (and adjust accordingly) in */etc/systemd/system/pbmcd.service*: 37 | 38 | ``` 39 | [Unit] 40 | Description = pbmcd service 41 | After = syslog.target 42 | After = network.target 43 | 44 | [Service] 45 | ExecStart = {{ PATH_TO_YOUR_VIRTUALENV }}/bin/pbmcd --foreground 46 | # Example (the environment should be baked into the interpreter for the venv, no need to activate): 47 | # ExecStart = /root/proxmoxbmc/.env/bin/pbmcd --foreground 48 | Restart = on-failure 49 | RestartSec = 2 50 | TimeoutSec = 120 51 | Type = simple 52 | # Optional if running as a different use don't forget to create one first 53 | # User = pbmc 54 | # Group = pbmc 55 | 56 | [Install] 57 | WantedBy = multi-user.target 58 | ``` 59 | Enable the service with: 60 | 61 | `# systemctl enable pbmcd` 62 | 63 | 64 | ## Limitations 65 | There are some limitations, or assumptions rather, regarding the ipmi requests for setting boot device and they are: 66 | * hd boot is assumed to be scsi0 67 | * cdrom is assumed to be ide2 (default when you create a VM with a cdrom attached) 68 | * network is assumed to be net0 69 | -------------------------------------------------------------------------------- /proxmoxbmc/__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('proxmoxbmc').version_string() 16 | -------------------------------------------------------------------------------- /proxmoxbmc/cmd/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/agnon/proxmoxbmc/aad30164e4f8507d89c3803e26d5f3207dc4e194/proxmoxbmc/cmd/__init__.py -------------------------------------------------------------------------------- /proxmoxbmc/cmd/pbmc.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 proxmoxbmc 24 | from proxmoxbmc import config as pbmc_config 25 | from proxmoxbmc.exception import ProxmoxBMCError 26 | from proxmoxbmc import log 27 | 28 | CONF = pbmc_config.get_config() 29 | 30 | LOG = log.get_logger() 31 | 32 | 33 | class ZmqClient(object): 34 | """Client part of the ProxmoxBMC system. 35 | 36 | The command-line client tool communicates with the server part 37 | of the ProxmoxBMC 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 pbmcd server on port ' 90 | '%(port)s, error: %(error)s' % {'port': server_port, 91 | 'error': ex}) 92 | LOG.error(msg) 93 | raise ProxmoxBMCError(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 ProxmoxBMCError(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 ProxmoxBMCError(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('vmid', 127 | help='The VMID 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('--proxmox-address', 147 | dest='proxmox_address', 148 | default=None, 149 | help=('The address to a proxmox node/VIP; defaults to ' 150 | 'None')) 151 | parser.add_argument('--token-user', 152 | dest='token_user', 153 | default="root@pam", 154 | help=('The user to which the API token belong; defaults to ' 155 | '"root@pam"')) 156 | parser.add_argument('--token-name', 157 | dest='token_name', 158 | default='pbmc', 159 | help=('The name of the API token; defaults to ' 160 | 'pbmc')) 161 | parser.add_argument('--token-value', 162 | dest='token_value', 163 | default=None, 164 | help=('The token value given when creating the API token; defaults to ' 165 | 'None')) 166 | return parser 167 | 168 | def take_action(self, args): 169 | 170 | log = logging.getLogger(__name__) 171 | 172 | self.app.zmq.communicate( 173 | 'add', args, no_daemon=self.app.options.no_daemon 174 | ) 175 | 176 | 177 | class DeleteCommand(Command): 178 | """Delete a virtual BMC for a virtual machine instance""" 179 | 180 | def get_parser(self, prog_name): 181 | parser = super(DeleteCommand, self).get_parser(prog_name) 182 | 183 | parser.add_argument('vmids', nargs='+', 184 | help='A list of virtual machine IDs') 185 | 186 | return parser 187 | 188 | def take_action(self, args): 189 | self.app.zmq.communicate('delete', args, self.app.options.no_daemon) 190 | 191 | 192 | class StartCommand(Command): 193 | """Start a proxmox BMC for a virtual machine instance""" 194 | 195 | def get_parser(self, prog_name): 196 | parser = super(StartCommand, self).get_parser(prog_name) 197 | 198 | parser.add_argument('vmids', nargs='+', 199 | help='A list of virtual machine IDs') 200 | 201 | return parser 202 | 203 | def take_action(self, args): 204 | self.app.zmq.communicate( 205 | 'start', args, no_daemon=self.app.options.no_daemon 206 | ) 207 | 208 | 209 | class StopCommand(Command): 210 | """Stop a proxmox BMC for a virtual machine instance""" 211 | 212 | def get_parser(self, prog_name): 213 | parser = super(StopCommand, self).get_parser(prog_name) 214 | 215 | parser.add_argument('vmids', nargs='+', 216 | help='A list of virtual machine IDs') 217 | 218 | return parser 219 | 220 | def take_action(self, args): 221 | self.app.zmq.communicate( 222 | 'stop', args, no_daemon=self.app.options.no_daemon 223 | ) 224 | 225 | 226 | class ListCommand(Lister): 227 | """List all proxmox BMC instances""" 228 | 229 | def take_action(self, args): 230 | rsp = self.app.zmq.communicate( 231 | 'list', args, no_daemon=self.app.options.no_daemon 232 | ) 233 | return rsp['header'], sorted(rsp['rows']) 234 | 235 | 236 | class ShowCommand(Lister): 237 | """Show proxmox BMC properties""" 238 | 239 | def get_parser(self, prog_name): 240 | parser = super(ShowCommand, self).get_parser(prog_name) 241 | 242 | parser.add_argument('vmid', 243 | help='The ID of the virtual machine') 244 | 245 | return parser 246 | 247 | def take_action(self, args): 248 | rsp = self.app.zmq.communicate( 249 | 'show', args, no_daemon=self.app.options.no_daemon 250 | ) 251 | return rsp['header'], sorted(rsp['rows']) 252 | 253 | 254 | class ProxmoxBMCApp(App): 255 | 256 | def __init__(self): 257 | super(ProxmoxBMCApp, self).__init__( 258 | description='Virtual Baseboard Management Controller (BMC) backed ' 259 | 'by virtual machines', 260 | version=proxmoxbmc.__version__, 261 | command_manager=CommandManager('proxmoxbmc'), 262 | deferred_help=True, 263 | ) 264 | 265 | def build_option_parser(self, description, version, argparse_kwargs=None): 266 | parser = super(ProxmoxBMCApp, self).build_option_parser( 267 | description, version, argparse_kwargs 268 | ) 269 | 270 | parser.add_argument('--no-daemon', 271 | action='store_true', 272 | help='Do not start pbmcd automatically') 273 | 274 | return parser 275 | 276 | def initialize_app(self, argv): 277 | self.zmq = ZmqClient() 278 | 279 | def clean_up(self, cmd, result, err): 280 | self.LOG.debug('clean_up %(name)s', {'name': cmd.__class__.__name__}) 281 | if err: 282 | self.LOG.debug('got an error: %(error)s', {'error': err}) 283 | 284 | 285 | def main(argv=sys.argv[1:]): 286 | pbmc_app = ProxmoxBMCApp() 287 | return pbmc_app.run(argv) 288 | 289 | 290 | if __name__ == '__main__': 291 | sys.exit(main()) 292 | -------------------------------------------------------------------------------- /proxmoxbmc/cmd/pbmcd.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 proxmoxbmc 19 | from proxmoxbmc import config as pbmc_config 20 | from proxmoxbmc import control 21 | from proxmoxbmc import log 22 | from proxmoxbmc import utils 23 | 24 | 25 | LOG = log.get_logger() 26 | 27 | CONF = pbmc_config.get_config() 28 | 29 | 30 | def main(argv=sys.argv[1:]): 31 | parser = argparse.ArgumentParser( 32 | prog='ProxmoxBMC server', 33 | description='A proxmox BMC server for controlling virtual instances', 34 | ) 35 | parser.add_argument('--version', action='version', 36 | version=proxmoxbmc.__version__) 37 | parser.add_argument('--foreground', 38 | action='store_true', 39 | default=False, 40 | help='Do not daemonize') 41 | parser.add_argument('--pidfile', help='Path to the PID file', required=False) 42 | parser.add_argument('--configdir', help='Path to the PID file', required=False) 43 | 44 | args = parser.parse_args(argv) 45 | if args.pidfile: 46 | pid_file = args.pidfile 47 | else: 48 | pid_file = CONF['default']['pid_file'] 49 | 50 | if args.configdir: 51 | CONF['default']['config_dir'] = args.configdir 52 | 53 | 54 | 55 | try: 56 | with open(pid_file) as f: 57 | pid = int(f.read()) 58 | 59 | os.kill(pid, 0) 60 | 61 | except Exception: 62 | pass 63 | 64 | else: 65 | LOG.error('server PID #%(pid)d still running', {'pid': pid}) 66 | return 1 67 | 68 | def wrap_with_pidfile(func, pid): 69 | dir_name = os.path.dirname(pid_file) 70 | 71 | if not os.path.exists(CONF['default']['config_dir']): 72 | os.makedirs(CONF['default']['config_dir'], mode=0o700) 73 | 74 | if not os.path.exists(dir_name): 75 | os.makedirs(dir_name, mode=0o700) 76 | 77 | try: 78 | with tempfile.NamedTemporaryFile(mode='w+t', dir=dir_name, 79 | delete=False) as f: 80 | f.write(str(pid)) 81 | os.rename(f.name, pid_file) 82 | 83 | func() 84 | 85 | except Exception as e: 86 | LOG.error('%(error)s', {'error': e}) 87 | return 1 88 | 89 | finally: 90 | try: 91 | os.unlink(pid_file) 92 | 93 | except Exception: 94 | pass 95 | 96 | if args.foreground: 97 | return wrap_with_pidfile(control.application, os.getpid()) 98 | 99 | else: 100 | with utils.detach_process() as pid: 101 | if pid > 0: 102 | return 0 103 | 104 | return wrap_with_pidfile(control.application, pid) 105 | 106 | 107 | if __name__ == '__main__': 108 | sys.exit(main()) 109 | -------------------------------------------------------------------------------- /proxmoxbmc/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 proxmoxbmc import utils 17 | 18 | __all__ = ['get_config'] 19 | 20 | _CONFIG_FILE_PATHS = ( 21 | os.environ.get('PROXMOXBMC_CONFIG', ''), 22 | os.path.join(os.path.expanduser('~'), '.pbmc', 'proxmoxbmc.conf'), 23 | '/etc/proxmoxbmc/proxmoxbmc.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 ProxmoxBMCConfig(object): 31 | 32 | DEFAULTS = { 33 | 'default': { 34 | 'show_passwords': 'false', 35 | 'config_dir': os.path.join( 36 | os.path.expanduser('~'), '.pbmc' 37 | ), 38 | 'pid_file': os.path.join( 39 | os.path.expanduser('~'), '.pbmc', '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': 3 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 = ProxmoxBMCConfig() 98 | CONFIG.initialize() 99 | 100 | return CONFIG 101 | -------------------------------------------------------------------------------- /proxmoxbmc/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 proxmoxbmc import config as pbmc_config 23 | from proxmoxbmc import exception 24 | from proxmoxbmc import log 25 | from proxmoxbmc.manager import ProxmoxBMCManager 26 | 27 | CONF = pbmc_config.get_config() 28 | 29 | LOG = log.get_logger() 30 | 31 | TIMER_PERIOD = 3000 # milliseconds 32 | 33 | 34 | def main_loop(pbmc_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 pBMC 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 | pbmc_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(pbmc_manager, data_in) 87 | 88 | except exception.ProxmoxBMCError 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(pbmc_manager, data_in): 119 | """Control CLI command dispatcher 120 | 121 | Calls pBMC 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 input 131 | token_value = data_in['token_value'] 132 | token_name = data_in['token_name'] 133 | token_user = data_in['token_user'] 134 | vmid = data_in['vmid'] 135 | 136 | if not all((token_value, token_name, token_user, vmid)): 137 | error = ("You need to pass in token user/name/value for this to work") 138 | return {'msg': [error], 'rc': 1} 139 | 140 | rc, msg = pbmc_manager.add(**data_in) 141 | 142 | return { 143 | 'rc': rc, 144 | 'msg': [msg] if msg else [] 145 | } 146 | 147 | elif command == 'delete': 148 | data_out = [pbmc_manager.delete(vmid) 149 | for vmid in set(data_in['vmids'])] 150 | return { 151 | 'rc': max(rc for rc, msg in data_out), 152 | 'msg': [msg for rc, msg in data_out if msg], 153 | } 154 | 155 | elif command == 'start': 156 | data_out = [pbmc_manager.start(vmid) 157 | for vmid in set(data_in['vmids'])] 158 | return { 159 | 'rc': max(rc for rc, msg in data_out), 160 | 'msg': [msg for rc, msg in data_out if msg], 161 | } 162 | 163 | elif command == 'stop': 164 | data_out = [pbmc_manager.stop(vmid) 165 | for vmid in set(data_in['vmids'])] 166 | return { 167 | 'rc': max(rc for rc, msg in data_out), 168 | 'msg': [msg for rc, msg in data_out if msg], 169 | } 170 | 171 | elif command == 'list': 172 | rc, tables = pbmc_manager.list() 173 | 174 | header = ('VMID', 'Status', 'Address', 'Port') 175 | keys = ('vmid', 'status', 'address', 'port') 176 | return { 177 | 'rc': rc, 178 | 'header': header, 179 | 'rows': [ 180 | [table.get(key, '?') for key in keys] for table in tables 181 | ] 182 | } 183 | 184 | elif command == 'show': 185 | rc, table = pbmc_manager.show(data_in['vmid']) 186 | 187 | return { 188 | 'rc': rc, 189 | 'header': ('Property', 'Value'), 190 | 'rows': table, 191 | } 192 | 193 | else: 194 | return { 195 | 'rc': 1, 196 | 'msg': ['Unknown command'], 197 | } 198 | 199 | 200 | def application(): 201 | """pbmcd application entry point 202 | 203 | Initializes, serves and cleans up everything. 204 | """ 205 | pbmc_manager = ProxmoxBMCManager() 206 | 207 | pbmc_manager.periodic() 208 | 209 | def kill_children(*args): 210 | pbmc_manager.periodic(shutdown=True) 211 | sys.exit(0) 212 | 213 | # SIGTERM does not seem to propagate to multiprocessing 214 | signal.signal(signal.SIGTERM, kill_children) 215 | 216 | try: 217 | main_loop(pbmc_manager, command_dispatcher) 218 | except KeyboardInterrupt: 219 | LOG.info('Got keyboard interrupt, exiting') 220 | pbmc_manager.periodic(shutdown=True) 221 | except Exception as ex: 222 | LOG.error( 223 | 'Control server error: %(error)s', {'error': ex} 224 | ) 225 | pbmc_manager.periodic(shutdown=True) 226 | -------------------------------------------------------------------------------- /proxmoxbmc/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 ProxmoxBMCError(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(ProxmoxBMCError, self).__init__(self.message) 24 | 25 | 26 | class VmIdAlreadyExists(ProxmoxBMCError): 27 | message = 'VMID %(vmid)s already exists' 28 | 29 | 30 | class VmIdNotFound(ProxmoxBMCError): 31 | message = 'No VMID with matching ID %(vmid)s was found' 32 | 33 | 34 | class DetachProcessError(ProxmoxBMCError): 35 | message = ('Error when forking (detaching) the ProxmoxBMC process ' 36 | 'from its parent and session. Error: %(error)s') 37 | -------------------------------------------------------------------------------- /proxmoxbmc/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 proxmoxbmc 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 ProxmoxBMCLogger(logging.Logger): 26 | 27 | def __init__(self, debug=False, logfile=None): 28 | logging.Logger.__init__(self, 'ProxmoxBMC') 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 = ProxmoxBMCLogger(debug=log_conf['debug'], 54 | logfile=log_conf['logfile']) 55 | 56 | return LOGGER 57 | -------------------------------------------------------------------------------- /proxmoxbmc/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 proxmoxbmc import config as pbmc_config 21 | from proxmoxbmc import exception 22 | from proxmoxbmc import log 23 | from proxmoxbmc import utils 24 | from proxmoxbmc.pbmc import ProxmoxBMC 25 | 26 | LOG = log.get_logger() 27 | 28 | # BMC status 29 | RUNNING = 'running' 30 | DOWN = 'down' 31 | ERROR = 'error' 32 | 33 | DEFAULT_SECTION = 'ProxmoxBMC' 34 | 35 | CONF = pbmc_config.get_config() 36 | 37 | 38 | class ProxmoxBMCManager(object): 39 | 40 | PBMC_OPTIONS = ['username', 'password', 'address', 'port', 41 | 'vmid', 'proxmox_address', 'token_user', 'token_name', 42 | 'token_value', 'active'] 43 | 44 | def __init__(self): 45 | super(ProxmoxBMCManager, self).__init__() 46 | self.config_dir = CONF['default']['config_dir'] 47 | self._running_vmids = {} 48 | 49 | def _parse_config(self, vmid): 50 | config_path = os.path.join(self.config_dir, vmid, 'config') 51 | if not os.path.exists(config_path): 52 | raise exception.VmIdNotFound(vmid=vmid) 53 | 54 | try: 55 | config = configparser.ConfigParser() 56 | config.read(config_path) 57 | 58 | bmc = {} 59 | for item in self.PBMC_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.VmIdNotFound(vmid=vmid) 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['vmid'], 'config' 85 | ) 86 | 87 | with open(config_path, 'w') as f: 88 | config.write(f) 89 | 90 | def _pbmc_enabled(self, vmid, lets_enable=None, config=None): 91 | if not config: 92 | config = self._parse_config(vmid) 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_pbmc_states(self, shutdown=False): 109 | """Starts/stops pBMC instances 110 | 111 | Walks over pBMC instances configuration, starts 112 | enabled but dead instances, kills non-configured 113 | but alive ones. 114 | """ 115 | 116 | def pbmc_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 | pbmc = ProxmoxBMC(**bmc_config) 130 | 131 | except Exception as ex: 132 | LOG.exception( 133 | 'Error running pBMC with configuration ' 134 | '%(opts)s: %(error)s', {'opts': show_options, 135 | 'error': ex} 136 | ) 137 | return 138 | 139 | try: 140 | pbmc.listen(timeout=CONF['ipmi']['session_timeout']) 141 | 142 | except Exception as ex: 143 | LOG.exception( 144 | 'Shutdown pBMC for vmid %(vmid)s, cause ' 145 | '%(error)s', {'vmid': show_options['vmid'], 146 | 'error': ex} 147 | ) 148 | return 149 | 150 | for vmid in os.listdir(self.config_dir): 151 | if not os.path.isdir( 152 | os.path.join(self.config_dir, vmid) 153 | ): 154 | continue 155 | 156 | try: 157 | bmc_config = self._parse_config(vmid) 158 | 159 | except exception.VmIdNotFound: 160 | continue 161 | 162 | if shutdown: 163 | lets_enable = False 164 | else: 165 | lets_enable = self._pbmc_enabled( 166 | vmid, config=bmc_config 167 | ) 168 | 169 | instance = self._running_vmids.get(vmid) 170 | 171 | if lets_enable: 172 | 173 | if not instance or not instance.is_alive(): 174 | 175 | instance = multiprocessing.Process( 176 | name='pbmcd-managing-vmid-%s' % vmid, 177 | target=pbmc_runner, 178 | args=(bmc_config,) 179 | ) 180 | 181 | instance.daemon = True 182 | instance.start() 183 | 184 | self._running_vmids[vmid] = instance 185 | 186 | LOG.info( 187 | 'Started pBMC instance for vmid ' 188 | '%(vmid)s', {'vmid': vmid} 189 | ) 190 | 191 | if not instance.is_alive(): 192 | LOG.debug( 193 | 'Found dead pBMC instance for vmid %(vmid)s ' 194 | '(rc %(rc)s)', {'vmid': vmid, 195 | 'rc': instance.exitcode} 196 | ) 197 | 198 | else: 199 | if instance: 200 | if instance.is_alive(): 201 | instance.terminate() 202 | LOG.info( 203 | 'Terminated pBMC instance for vmid ' 204 | '%(vmid)s', {'vmid': vmid} 205 | ) 206 | 207 | self._running_vmids.pop(vmid, None) 208 | 209 | def _show(self, vmid): 210 | bmc_config = self._parse_config(vmid) 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_vmids.get(vmid) 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_pbmc_states(shutdown) 232 | 233 | def add(self, username, password, port, address, vmid, proxmox_address, 234 | token_user, token_name, token_value, **kwargs): 235 | 236 | # check libvirt's connection and if domain exist prior to adding it 237 | # utils.check_libvirt_connection_and_domain( 238 | # libvirt_uri, domain_name, 239 | # sasl_username=libvirt_sasl_username, 240 | # sasl_password=libvirt_sasl_password) 241 | 242 | vmid_path = os.path.join(self.config_dir, vmid) 243 | 244 | try: 245 | os.makedirs(vmid_path) 246 | except OSError as ex: 247 | if ex.errno == errno.EEXIST: 248 | return 1, str(ex) 249 | 250 | msg = ('Failed to create vmid %(vmid)s. ' 251 | 'Error: %(error)s' % {'vmid': vmid, 'error': ex}) 252 | LOG.error(msg) 253 | return 1, msg 254 | 255 | try: 256 | self._store_config(vmid=str(vmid), 257 | username=username, 258 | password=password, 259 | port=str(port), 260 | address=address, 261 | proxmox_address=proxmox_address, 262 | token_user=token_user, 263 | token_name=token_name, 264 | token_value=token_value, 265 | active=False) 266 | 267 | except Exception as ex: 268 | self.delete(vmid) 269 | return 1, str(ex) 270 | 271 | return 0, '' 272 | 273 | def delete(self, vmid): 274 | vmid_path = os.path.join(self.config_dir, vmid) 275 | if not os.path.exists(vmid_path): 276 | raise exception.VmIdNotFound(vmid=vmid) 277 | 278 | try: 279 | self.stop(vmid) 280 | except exception.ProxmoxBMCError: 281 | pass 282 | 283 | shutil.rmtree(vmid_path) 284 | 285 | return 0, '' 286 | 287 | def start(self, vmid): 288 | try: 289 | bmc_config = self._parse_config(vmid) 290 | 291 | except Exception as ex: 292 | return 1, str(ex) 293 | 294 | if vmid in self._running_vmids: 295 | 296 | self._sync_pbmc_states() 297 | 298 | if vmid in self._running_vmids: 299 | LOG.warning( 300 | 'BMC instance %(vmid)s already running, ignoring ' 301 | '"start" command' % {'vmid': vmid}) 302 | return 0, '' 303 | 304 | try: 305 | self._pbmc_enabled(vmid, 306 | config=bmc_config, 307 | lets_enable=True) 308 | 309 | except Exception as e: 310 | LOG.exception('Failed to start vmid %s', vmid) 311 | return 1, ('Failed to start vmid %(vmid)s. Error: ' 312 | '%(error)s' % {'vmid': vmid, 'error': e}) 313 | 314 | self._sync_pbmc_states() 315 | 316 | return 0, '' 317 | 318 | def stop(self, vmid): 319 | try: 320 | self._pbmc_enabled(vmid, lets_enable=False) 321 | 322 | except Exception as ex: 323 | LOG.exception('Failed to stop vmid %s', vmid) 324 | return 1, str(ex) 325 | 326 | self._sync_pbmc_states() 327 | 328 | return 0, '' 329 | 330 | def list(self): 331 | rc = 0 332 | tables = [] 333 | try: 334 | for vmid in os.listdir(self.config_dir): 335 | if os.path.isdir(os.path.join(self.config_dir, vmid)): 336 | tables.append(self._show(vmid)) 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, vmid): 345 | return 0, list(self._show(vmid).items()) 346 | -------------------------------------------------------------------------------- /proxmoxbmc/pbmc.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 pyghmi.ipmi.bmc as bmc 14 | import re 15 | 16 | from proxmoxer import ProxmoxAPI 17 | 18 | #from proxmoxbmc import exception 19 | from proxmoxbmc import log 20 | #from proxmoxbmc 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 | # ide2 == cdrom. It's common in PVE 39 | GET_BOOT_DEVICES_MAP = { 40 | 'net': 4, 41 | 'scsi': 8, 42 | 'ide': 8, 43 | 'cdrom': 0x14 44 | } 45 | 46 | SET_BOOT_DEVICES_MAP = { 47 | 'network': 'net0', 48 | 'hd': 'scsi0', 49 | 'optical': 'ide2', 50 | } 51 | 52 | 53 | class ProxmoxBMC(bmc.Bmc): 54 | 55 | def __init__(self, username, password, port, address, 56 | vmid, proxmox_address, token_user, token_name, token_value, **kwargs): 57 | super(ProxmoxBMC, self).__init__({username: password}, 58 | port=port, address=address) 59 | self.vmid = vmid 60 | 61 | # TODO check kwargs for verify_ssl and use if set 62 | self._proxmox = ProxmoxAPI(proxmox_address, user=token_user, token_name=token_name, token_value=token_value, verify_ssl=False) 63 | 64 | def _locate_vmid(self): 65 | for pve_node in self._proxmox.nodes.get(): 66 | if str(pve_node['status']) == 'online': 67 | for vm in self._proxmox.nodes(pve_node['node']).qemu.get(): 68 | if str(vm['vmid']) == self.vmid: 69 | return pve_node 70 | 71 | return None 72 | 73 | def get_boot_device(self): 74 | LOG.debug('Get boot device called for %(vmid)s', {'vmid': self.vmid}) 75 | 76 | # First we find where in the cluster the VMID is located 77 | pve_node = self._locate_vmid() 78 | 79 | if (pve_node): 80 | config = self._proxmox.nodes(pve_node['node']).qemu(f'{self.vmid}').config.get() 81 | boot_device = re.match(r"^order=([a-z]+)", config['boot']) 82 | if not boot_device.group(1): 83 | LOG.error('No boot device selected for VM %(vmid)s', {'vmid': self.vmid}) 84 | 85 | if (boot_device.group(1) == 'ide'): 86 | boot_device_with_number = re.match(r"^order=([a-z0-9]+)", config['boot']) 87 | if boot_device_with_number.group(1) == 'ide2': 88 | return GET_BOOT_DEVICES_MAP['cdrom'] 89 | 90 | return GET_BOOT_DEVICES_MAP.get(boot_device.group(1), 0) 91 | 92 | def set_boot_device(self, bootdevice): 93 | LOG.debug('Set boot device called for %(vmid)s with boot ' 94 | 'device "%(bootdev)s"', {'vmid': self.vmid, 95 | 'bootdev': bootdevice}) 96 | device = SET_BOOT_DEVICES_MAP.get(bootdevice) 97 | if device is None: 98 | # Invalid data field in request 99 | return IPMI_INVALID_DATA 100 | 101 | pve_node = self._locate_vmid() 102 | 103 | if (pve_node): 104 | self._proxmox.nodes(pve_node['node']).qemu(f'{self.vmid}').config.post(boot=f'order={device}') 105 | 106 | def get_power_state(self): 107 | LOG.debug('Get power state called for %(vmid)s', 108 | {'vmid': self.vmid}) 109 | 110 | pve_node = self._locate_vmid() 111 | if (pve_node): 112 | current_status = self._proxmox.nodes(pve_node['node']).qemu(f'{self.vmid}').status.current.get() 113 | if current_status['status'] == 'running': 114 | return POWERON 115 | 116 | return POWEROFF 117 | 118 | def pulse_diag(self): 119 | LOG.debug('Power diag called for %(vmid)s (noop)', 120 | {'vmid': self.vmid}) 121 | 122 | def power_off(self): 123 | LOG.debug('Power off called for %(vmid)s', 124 | {'vmid': self.vmid}) 125 | 126 | pve_node = self._locate_vmid() 127 | if (pve_node): 128 | self._proxmox.nodes(pve_node['node']).qemu(f'{self.vmid}').status.stop.post() 129 | 130 | def power_on(self): 131 | LOG.debug('Power on called for %(vmid)s', 132 | {'vmid': self.vmid}) 133 | 134 | pve_node = self._locate_vmid() 135 | if (pve_node): 136 | self._proxmox.nodes(pve_node['node']).qemu(f'{self.vmid}').status.start.post() 137 | 138 | def power_shutdown(self): 139 | LOG.debug('Soft power off called for %(vmid)s', 140 | {'vmid': self.vmid}) 141 | 142 | pve_node = self._locate_vmid() 143 | if (pve_node): 144 | if (self.get_power_state() == POWERON): 145 | self._proxmox.nodes(pve_node['node']).qemu(f'{self.vmid}').status.shutdown.post() 146 | 147 | def power_reset(self): 148 | LOG.debug('Power reset called for %(vmid)s', 149 | {'vmid': self.vmid}) 150 | 151 | pve_node = self._locate_vmid() 152 | if (pve_node): 153 | if (self.get_power_state() == POWERON): 154 | self._proxmox.nodes(pve_node['node']).qemu(f'{self.vmid}').status.reset.post() 155 | 156 | -------------------------------------------------------------------------------- /proxmoxbmc/utils.py: -------------------------------------------------------------------------------- 1 | # Licensed under the Apache License, Version 2.0 (the "License"); you may 2 | # not use this file except in compliance with the License. You may obtain 3 | # a copy of the License at 4 | # 5 | # http://www.apache.org/licenses/LICENSE-2.0 6 | # 7 | # Unless required by applicable law or agreed to in writing, software 8 | # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 9 | # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 10 | # License for the specific language governing permissions and limitations 11 | # under the License. 12 | 13 | import os 14 | import sys 15 | 16 | from proxmoxbmc import exception 17 | 18 | from proxmoxer import ProxmoxAPI 19 | 20 | # def locate_vmid(self, vmid): 21 | # proxmox = ProxmoxAPI(proxmox_address, user=proxmox_user, token_name=proxmox_token_name, token_value=proxmox_token_value, verify_ssl=False) 22 | # for pve_node in self._proxmox.nodes.get(): 23 | # for vm in self._proxmox.nodes(pve_node['node']).qemu.get(): 24 | # if vm['vmid'] == self.vmid: 25 | # return pve_node 26 | 27 | # return None 28 | 29 | def is_pid_running(pid): 30 | try: 31 | os.kill(pid, 0) 32 | return True 33 | except OSError: 34 | return False 35 | 36 | 37 | def str2bool(string): 38 | lower = string.lower() 39 | if lower not in ('true', 'false'): 40 | raise ValueError('Value "%s" can not be interpreted as ' 41 | 'boolean' % string) 42 | return lower == 'true' 43 | 44 | 45 | def mask_dict_password(dictionary, secret='***'): 46 | """Replace passwords with a secret in a dictionary.""" 47 | d = dictionary.copy() 48 | for k in d: 49 | if 'password' in k: 50 | d[k] = secret 51 | return d 52 | 53 | 54 | class detach_process(object): 55 | """Detach the process from its parent and session.""" 56 | 57 | def _fork(self, parent_exits): 58 | try: 59 | pid = os.fork() 60 | if pid > 0 and parent_exits: 61 | os._exit(0) 62 | 63 | return pid 64 | 65 | except OSError as e: 66 | raise exception.DetachProcessError(error=e) 67 | 68 | def _change_root_directory(self): 69 | """Change to root directory. 70 | 71 | Ensure that our process doesn't keep any directory in use. Failure 72 | to do this could make it so that an administrator couldn't 73 | unmount a filesystem, because it was our current directory. 74 | """ 75 | try: 76 | os.chdir('/') 77 | except Exception as e: 78 | error = ('Failed to change root directory. Error: %s' % e) 79 | raise exception.DetachProcessError(error=error) 80 | 81 | def _change_file_creation_mask(self): 82 | """Set the umask for new files. 83 | 84 | Set the umask for new files the process creates so that it does 85 | have complete control over the permissions of them. We don't 86 | know what umask we may have inherited. 87 | """ 88 | try: 89 | os.umask(0) 90 | except Exception as e: 91 | error = ('Failed to change file creation mask. Error: %s' % e) 92 | raise exception.DetachProcessError(error=error) 93 | 94 | def __enter__(self): 95 | pid = self._fork(parent_exits=False) 96 | if pid > 0: 97 | return pid 98 | 99 | os.setsid() 100 | 101 | self._fork(parent_exits=True) 102 | 103 | self._change_root_directory() 104 | self._change_file_creation_mask() 105 | 106 | sys.stdout.flush() 107 | sys.stderr.flush() 108 | 109 | si = open(os.devnull, 'r') 110 | so = open(os.devnull, 'a+') 111 | se = open(os.devnull, 'a+') 112 | 113 | os.dup2(si.fileno(), sys.stdin.fileno()) 114 | os.dup2(so.fileno(), sys.stdout.fileno()) 115 | os.dup2(se.fileno(), sys.stderr.fileno()) 116 | 117 | return pid 118 | 119 | def __exit__(self, type, value, traceback): 120 | pass 121 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | # The order of packages is significant, because pip processes them in the order 2 | # of appearance. Changing the order has an impact on the overall integration 3 | # process, which may cause wedges in the gate later. 4 | 5 | pbr!=2.1.0,>=2.0.0 # Apache-2.0 6 | pyghmi>=1.2.0 # Apache-2.0 7 | cliff!=2.9.0,>=2.8.0 # Apache-2.0 8 | pyzmq>=19.0.0 # LGPL+BSD 9 | proxmoxer==1.3.0 10 | requests==2.27.1 -------------------------------------------------------------------------------- /setup.cfg: -------------------------------------------------------------------------------- 1 | [metadata] 2 | name = proxmoxbmc 3 | summary = Create virtual BMCs for controlling virtual instances via IPMI in Proxmox VE. Based on VirtualBMC https://github.com/openstack/virtualbmc. 4 | version = 1.0.1 5 | description_file = README.md 6 | author = Marcus Nordenberg 7 | author_email = marcus.nordenberg@gmail.com 8 | python_requires = >=3.6 9 | classifier = 10 | Environment :: Proxmox 11 | Intended Audience :: Information Technology 12 | Intended Audience :: System Administrators 13 | License :: OSI Approved :: Apache Software License 14 | Operating System :: POSIX :: Linux 15 | Programming Language :: Python 16 | Programming Language :: Python :: Implementation :: CPython 17 | Programming Language :: Python :: 3 :: Only 18 | Programming Language :: Python :: 3 19 | Programming Language :: Python :: 3.6 20 | Programming Language :: Python :: 3.7 21 | Programming Language :: Python :: 3.8 22 | Programming Language :: Python :: 3.9 23 | 24 | [files] 25 | packages = 26 | proxmoxbmc 27 | 28 | [entry_points] 29 | console_scripts = 30 | pbmc = proxmoxbmc.cmd.pbmc:main 31 | pbmcd = proxmoxbmc.cmd.pbmcd:main 32 | 33 | proxmoxbmc = 34 | add = proxmoxbmc.cmd.pbmc:AddCommand 35 | delete = proxmoxbmc.cmd.pbmc:DeleteCommand 36 | start = proxmoxbmc.cmd.pbmc:StartCommand 37 | stop = proxmoxbmc.cmd.pbmc:StopCommand 38 | list = proxmoxbmc.cmd.pbmc:ListCommand 39 | show = proxmoxbmc.cmd.pbmc:ShowCommand 40 | -------------------------------------------------------------------------------- /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 | #!/usr/bin/env python 17 | 18 | from setuptools import setup 19 | 20 | setup( 21 | setup_requires=['pbr'], 22 | pbr=True, 23 | ) --------------------------------------------------------------------------------