Add the "vbmc" utility

This patch is refactoring the code and adding the vbmc utility.
This commit is contained in:
Lucas Alvares Gomes 2016-02-12 17:13:18 +00:00
parent d985f52cc6
commit f6e7153e2b
11 changed files with 555 additions and 190 deletions

4
.gitignore vendored
View File

@ -1,3 +1,7 @@
*.py[co]
.*.swp
*~
build
dist
*egg-info
*egg

33
README.md Normal file
View File

@ -0,0 +1,33 @@
Virtual BMC
===========
A virtual BMC for controlling virtual machines using IPMI commands.
Installation
------------
```bash
pip install virtualbmc
```
Usage
-----
![alt text](images/demo.gif "Virtual BMC demo")
Other supported commands
------------------------
```bash
# Power the virtual machine on or off
ipmitool -I lanplus -U admin -P password -H 127.0.0.1 power on|off
# Check the power status
ipmitool -I lanplus -U admin -P password -H 127.0.0.1 power status
# Set the boot device to network, hd or cdrom
ipmitool -I lanplus -U admin -P password -H 127.0.0.1 chassis bootdev pxe|disk|cdrom
# Get the current boot device
ipmitool -I lanplus -U admin -P password -H 127.0.0.1 chassis bootparam get 5
```

View File

@ -1,27 +0,0 @@
Virtual BMC
===========
A virtual BMC for controlling virtual machines with IPMI commands
Usage
-----
#. Create a virtual machine
#. Run the virtual BMC::
python ./virtualbmc.py --domain-name <virtual machine name>
#. Control it via IPMI::
# Power the virtual machine on or off
ipmitool -I lanplus -U admin -P password -H 127.0.0.1 power on|off
# Check the power status
ipmitool -I lanplus -U admin -P password -H 127.0.0.1 power status
# Set the boot device to network, hd or cdrom
ipmitool -I lanplus -U admin -P password -H 127.0.0.1 chassis bootdev pxe|disk|cdrom
# Get the current boot device
ipmitool -I lanplus -U admin -P password -H 127.0.0.1 chassis bootparam get 5

BIN
images/demo.gif Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 298 KiB

40
setup.py Normal file
View File

@ -0,0 +1,40 @@
# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
# a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.
from setuptools import setup
setup(name='virtualbmc',
version='0.0.1',
description=('Create virtual BMCs for controlling virtual instances '
'via IPMI'),
url='http://github.com/umago/virtualbmc',
author='Lucas Alvares Gomes',
author_email='lucasagomes@gmail.com',
license='Apache License 2.0',
packages=['virtualbmc', 'virtualbmc.cmd'],
install_requires=['six',
'python-daemon',
'prettytable',
'libvirt-python',
'pyghmi>=0.9.8'],
entry_points = {
'console_scripts': [
'vbmc = virtualbmc.cmd.vbmc:main',
],
},
classifiers=[
'Development Status :: 3 - Alpha',
'Topic :: Utilities',
'License :: OSI Approved :: Apache Software License',
'Programming Language :: Python',
],
)

View File

@ -1,163 +0,0 @@
# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
# a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.
import argparse
import signal
import sys
import xml.etree.ElementTree as ET
import libvirt
import pyghmi.ipmi.bmc as bmc
# Power states
POWEROFF = 0
POWERON = 1
# Boot device maps
GET_BOOT_DEVICES_MAP = {
'network': 4,
'hd': 8,
'cdrom': 0x14,
}
SET_BOOT_DEVICES_MAP = {
'network': 'network',
'hd': 'hd',
'optical': 'cdrom',
}
class VirtualBMC(bmc.Bmc):
def __init__(self, username, password, port, address,
domain_name, libvirt_uri):
super(VirtualBMC, self).__init__({username: password},
port=port, address=address)
self.domain_name = domain_name
# Create the connection
self.conn = libvirt.open(libvirt_uri)
if self.conn is None:
print('Failed to open connection to the hypervisor '
'using the libvirt URI: "%s"' % libvirt_uri)
sys.exit(1)
try:
self.domain = self.conn.lookupByName(self.domain_name)
except libvirt.libvirtError:
print('Failed to find the domain "%s"' % self.domain_name)
sys.exit(1)
def get_boot_device(self):
parsed = ET.fromstring(self.domain.XMLDesc())
boot_devs = parsed.findall('.//os/boot')
boot_dev = boot_devs[0].attrib['dev']
return GET_BOOT_DEVICES_MAP.get(boot_dev, 0)
def set_boot_device(self, bootdevice):
device = SET_BOOT_DEVICES_MAP.get(bootdevice)
if device is None:
print('Uknown boot device: %s' % bootdevice)
return 0xd5
parsed = ET.fromstring(self.domain.XMLDesc())
os = parsed.find('os')
boot_list = os.findall('boot')
# Clear boot list
for boot_el in boot_list:
os.remove(boot_el)
boot_el = ET.SubElement(os, 'boot')
boot_el.set('dev', device)
try:
self.conn.defineXML(ET.tostring(parsed))
except libvirt.libvirtError as e:
print('Failed setting the boot device to "%s" on the '
'"%s" domain' % (device, self.domain_name))
def get_power_state(self):
try:
if self.domain.isActive():
return POWERON
except libvirt.libvirtError as e:
print('Error getting the power state of domain "%s". '
'Error: %s' % (self.domain_name, e))
return
return POWEROFF
def power_off(self):
try:
self.domain.destroy()
except libvirt.libvirtError as e:
print('Error powering off the domain "%s". Error: %s' %
(self.domain_name, e))
return
def power_on(self):
try:
self.domain.create()
except libvirt.libvirtError as e:
print('Error powering on the domain "%s". Error: %s' %
(self.domain_name, e))
return
def signal_handler(signal, frame):
print('SIGINT received, stopping the Virtual BMC...')
sys.exit(0)
if __name__ == '__main__':
parser = argparse.ArgumentParser(
prog='Virtual BMC',
description='Virtual BMC for controlling virtual instances',
)
parser.add_argument('--username',
dest='username',
default='admin',
help='The BMC username; defaults to "admin"')
parser.add_argument('--password',
dest='password',
default='password',
help='The BMC password; defaults to "password"')
parser.add_argument('--port',
dest='port',
type=int,
default=623,
help='Port to listen on; defaults to 623')
parser.add_argument('--address',
dest='address',
default='::',
help='Address to bind to; defaults to ::')
parser.add_argument('--domain-name',
dest='domain_name',
required=True,
help='The name of the virtual machine')
parser.add_argument('--libvirt-uri',
dest='libvirt_uri',
default="qemu:///system",
help='The libvirt URI; defaults to "qemu:///system"')
args = parser.parse_args()
vbmc = VirtualBMC(username=args.username,
password=args.password,
port=args.port,
address=args.address,
domain_name=args.domain_name,
libvirt_uri=args.libvirt_uri)
# Register the signal handler
signal.signal(signal.SIGINT, signal_handler)
# Start the virtual BMC
vbmc.listen()

20
virtualbmc/__init__.py Normal file
View File

@ -0,0 +1,20 @@
# Copyright 2016 Red Hat, Inc.
# All Rights Reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
# a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.
__all__ = ['exception', 'VirtualBMC', 'VirtualBMCManager']
import exception
from virtualbmc import VirtualBMC
from virtualbmc import VirtualBMCManager

View File

127
virtualbmc/cmd/vbmc.py Normal file
View File

@ -0,0 +1,127 @@
# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
# a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.
from __future__ import print_function
import argparse
import sys
from prettytable import PrettyTable
from virtualbmc import exception
from virtualbmc import VirtualBMCManager
def main():
parser = argparse.ArgumentParser(
prog='Virtual BMC',
description='A virtual BMC for controlling virtual instances',
)
subparsers = parser.add_subparsers()
# create the parser for the "add" command
parser_add = subparsers.add_parser('add', help='Add a new virtual BMC')
parser_add.set_defaults(command='add')
parser_add.add_argument('domain_name',
help='The name of the virtual machine')
parser_add.add_argument('--username',
dest='username',
default='admin',
help='The BMC username; defaults to "admin"')
parser_add.add_argument('--password',
dest='password',
default='password',
help='The BMC password; defaults to "password"')
parser_add.add_argument('--port',
dest='port',
type=int,
default=623,
help='Port to listen on; defaults to 623')
parser_add.add_argument('--address',
dest='address',
default='::',
help='Address to bind to; defaults to ::')
parser_add.add_argument('--libvirt-uri',
dest='libvirt_uri',
default="qemu:///system",
help=('The libvirt URI; defaults to '
'"qemu:///system"'))
# create the parser for the "delete" command
parser_delete = subparsers.add_parser('delete',
help='Delete a virtual BMC')
parser_delete.set_defaults(command='delete')
parser_delete.add_argument('domain_name',
help='The name of the virtual machine')
# create the parser for the "start" command
parser_start = subparsers.add_parser('start', help='Start a virtual BMC')
parser_start.set_defaults(command='start')
parser_start.add_argument('domain_name',
help='The name of the virtual machine')
# create the parser for the "stop" command
parser_stop = subparsers.add_parser('stop', help='Stop a virtual BMC')
parser_stop.set_defaults(command='stop')
parser_stop.add_argument('domain_name',
help='The name of the virtual machine')
# create the parser for the "list" command
parser_stop = subparsers.add_parser('list', help='list all virtual BMCs')
parser_stop.set_defaults(command='list')
# create the parser for the "show" command
parser_stop = subparsers.add_parser('show', help='Show a virtual BMCs')
parser_stop.set_defaults(command='show')
parser_stop.add_argument('domain_name',
help='The name of the virtual machine')
args = parser.parse_args()
manager = VirtualBMCManager()
try:
if args.command == 'add':
manager.add(username=args.username, password=args.password,
port=args.port, address=args.address,
domain_name=args.domain_name,
libvirt_uri=args.libvirt_uri)
elif args.command == 'delete':
manager.delete(args.domain_name)
elif args.command == 'start':
manager.start(args.domain_name)
elif args.command == 'stop':
manager.stop(args.domain_name)
elif args.command == 'list':
ptable = PrettyTable(['Domain name', 'Status', 'Address', 'Port'])
for bmc in manager.list():
ptable.add_row([bmc['domain_name'], bmc['status'],
bmc['address'], bmc['port']])
print(ptable)
elif args.command == 'show':
bmc = manager.show(args.domain_name)
for key, val in bmc.items():
ptable.add_row([key, val])
print(ptable)
except (exception.LibvirtConnectionOpenError,
exception.DomainNotFound) as e:
print(e, file=sys.stderr)
exit(1)
if __name__ == '__main__':
main()

31
virtualbmc/exception.py Normal file
View File

@ -0,0 +1,31 @@
# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
# a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.
class VirtualBMCError(Exception):
message = None
def __init__(self, message=None, **kwargs):
if self.message and kwargs:
self.message = self.message % kwargs
super(VirtualBMCError, self).__init__(self.message)
class DomainNotFound(VirtualBMCError):
message = 'No domain with matching name %(domain)s was found'
class LibvirtConnectionOpenError(VirtualBMCError):
message = ('Fail to establish a connection with libvirt URI "%(uri)s". '
'Error: %(error)s')

300
virtualbmc/virtualbmc.py Normal file
View File

@ -0,0 +1,300 @@
# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
# a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.
from __future__ import print_function
import errno
import os
import signal
import shutil
import sys
import xml.etree.ElementTree as ET
import daemon
import libvirt
import pyghmi.ipmi.bmc as bmc
from six.moves import configparser
import exception
# Power states
POWEROFF = 0
POWERON = 1
# BMC status
RUNNING = 'running'
DOWN = 'down'
# Boot device maps
GET_BOOT_DEVICES_MAP = {
'network': 4,
'hd': 8,
'cdrom': 0x14,
}
SET_BOOT_DEVICES_MAP = {
'network': 'network',
'hd': 'hd',
'optical': 'cdrom',
}
DEFAULT_SECTION = 'VirtualBMC'
CONFIG_PATH = os.path.join(os.path.expanduser('~'), '.vbmc')
class libvirt_open(object):
def __init__(self, uri, readonly=False):
self.uri = uri
self.readonly = readonly
def __enter__(self):
try:
if self.readonly:
self.conn = libvirt.openReadOnly(self.uri)
else:
self.conn = libvirt.open(self.uri)
return self.conn
except libvirt.libvirtError as e:
raise exception.LibvirtConnectionOpenError(uri=self.uri, error=e)
def __exit__(self, type, value, traceback):
self.conn.close()
def get_libvirt_domain(conn, domain):
try:
return conn.lookupByName(domain)
except libvirt.libvirtError:
raise exception.DomainNotFound(domain=domain)
def check_libvirt_connection_and_domain(uri, domain):
with libvirt_open(uri, readonly=True) as conn:
get_libvirt_domain(conn, domain)
def is_pid_running(pid):
try:
os.kill(pid, 0)
return True
except OSError:
return False
class VirtualBMC(bmc.Bmc):
def __init__(self, username, password, port, address,
domain_name, libvirt_uri):
super(VirtualBMC, self).__init__({username: password},
port=port, address=address)
self.libvirt_uri = libvirt_uri
self.domain_name = domain_name
def get_boot_device(self):
with libvirt_open(self.libvirt_uri, readonly=True) as conn:
domain = get_libvirt_domain(conn, self.domain_name)
parsed = ET.fromstring(domain.XMLDesc())
boot_devs = parsed.findall('.//os/boot')
boot_dev = boot_devs[0].attrib['dev']
return GET_BOOT_DEVICES_MAP.get(boot_dev, 0)
def set_boot_device(self, bootdevice):
device = SET_BOOT_DEVICES_MAP.get(bootdevice)
if device is None:
print('Uknown boot device: %s' % bootdevice)
return 0xd5
with libvirt_open(self.libvirt_uri) as conn:
domain = get_libvirt_domain(conn, self.domain_name)
parsed = ET.fromstring(domain.XMLDesc())
os = parsed.find('os')
boot_list = os.findall('boot')
# Clear boot list
for boot_el in boot_list:
os.remove(boot_el)
boot_el = ET.SubElement(os, 'boot')
boot_el.set('dev', device)
try:
conn.defineXML(ET.tostring(parsed))
except libvirt.libvirtError as e:
print('Failed setting the boot device to "%s" on the '
'"%s" domain' % (device, self.domain_name),
file=sys.stderr)
def get_power_state(self):
try:
with libvirt_open(self.libvirt_uri, readonly=True) as conn:
domain = get_libvirt_domain(conn, self.domain_name)
if domain.isActive():
return POWERON
except libvirt.libvirtError as e:
print('Error getting the power state of domain "%s". '
'Error: %s' % (self.domain_name, e), file=sys.stderr)
return
return POWEROFF
def power_off(self):
try:
with libvirt_open(self.libvirt_uri) as conn:
domain = get_libvirt_domain(conn, self.domain_name)
domain.destroy()
except libvirt.libvirtError as e:
print('Error powering off the domain "%s". Error: %s' %
(self.domain_name, e), file=sys.stderr)
return
def power_on(self):
try:
with libvirt_open(self.libvirt_uri) as conn:
domain = get_libvirt_domain(conn, self.domain_name)
domain.create()
except libvirt.libvirtError as e:
print('Error powering on the domain "%s". Error: %s' %
(self.domain_name, e))
return
class VirtualBMCManager(object):
def _parse_config(self, domain_name):
config_path = os.path.join(CONFIG_PATH, domain_name, 'config')
if not os.path.exists(config_path):
raise exception.DomainNotFound(domain=domain_name)
config = configparser.ConfigParser()
config.read(config_path)
bmc = {}
for item in ('username', 'password', 'address',
'domain_name', 'libvirt_uri'):
bmc[item] = config.get(DEFAULT_SECTION, item)
# Port needs to be int
bmc['port'] = int(config.get(DEFAULT_SECTION, 'port'))
return bmc
def _show(self, domain_name):
running = False
try:
pidfile_path = os.path.join(CONFIG_PATH, domain_name, 'pid')
with open(pidfile_path, 'r') as f:
pid = int(f.read())
running = is_pid_running(pid)
except IOError:
pass
bmc_config = self._parse_config(domain_name)
bmc_config['status'] = RUNNING if running else DOWN
return bmc_config
def add(self, username, password, port, address,
domain_name, libvirt_uri):
# Check libvirt and domain
check_libvirt_connection_and_domain(libvirt_uri, domain_name)
domain_path = os.path.join(CONFIG_PATH, domain_name)
try:
os.makedirs(domain_path)
except OSError as e:
if e.errno == errno.EEXIST:
sys.exit('Domain %s already exist' % domain_name)
config_path = os.path.join(domain_path, 'config')
with open(config_path, 'w') as f:
config = configparser.ConfigParser()
config.add_section(DEFAULT_SECTION)
config.set(DEFAULT_SECTION, 'username', username)
config.set(DEFAULT_SECTION, 'password', password)
config.set(DEFAULT_SECTION, 'port', port)
config.set(DEFAULT_SECTION, 'address', address)
config.set(DEFAULT_SECTION, 'domain_name', domain_name)
config.set(DEFAULT_SECTION, 'libvirt_uri', libvirt_uri)
config.write(f)
def delete(self, domain_name):
domain_path = os.path.join(CONFIG_PATH, domain_name)
if not os.path.exists(domain_path):
raise exception.DomainNotFound(domain=domain_name)
self.stop(domain_name)
shutil.rmtree(domain_path)
def start(self, domain_name):
domain_path = os.path.join(CONFIG_PATH, domain_name)
if not os.path.exists(domain_path):
raise exception.DomainNotFound(domain=domain_name)
bmc_config = self._parse_config(domain_name)
# Check libvirt and domain
check_libvirt_connection_and_domain(
bmc_config['libvirt_uri'], domain_name)
pidfile_path = os.path.join(domain_path, 'pid')
with daemon.DaemonContext(stderr=sys.stderr):
# FIXME(lucasagomes): pyghmi start the sockets when the
# class is instantiated, therefore we need to create the object
# within the daemon context
vbmc = VirtualBMC(**bmc_config)
# Save the PID number
with open(pidfile_path, 'w') as f:
f.write(str(os.getpid()))
vbmc.listen()
def stop(sel, domain_name):
domain_path = os.path.join(CONFIG_PATH, domain_name)
if not os.path.exists(domain_path):
raise exception.DomainNotFound(domain=domain_name)
pidfile_path = os.path.join(domain_path, 'pid')
pid = None
try:
with open(pidfile_path, 'r') as f:
pid = int(f.read())
except IOError:
# LOG
return
else:
os.remove(pidfile_path)
try:
os.kill(pid, signal.SIGKILL)
except OSError:
pass
def list(self):
bmcs = []
try:
for domain in os.listdir(CONFIG_PATH):
bmcs.append(self._show(domain))
except OSError as e:
if e.errno == errno.EEXIST:
return bmcs
return bmcs
def show(self, domain_name):
return self._show(domain_name)