multiprocess server, ZMQ-based management cli tool

Original design of the VirtualBMC tool was that user manages
config files for individual VirtualBMC (via a cli tool), then
requests the tool to start the instances representing
individual VirtualBMC instances (via the cli tool). Then
the instances become independent processes. The only way
to know their whereabouts is through the pidfiles
they maintain.

There were certain practical inconveniences with the
original design, namely:

* Cumbersome to start/stop/monitor free-standing
  vBMC instances processes
* No two-way communication between the parent process
  and the VirtualBMC instances what makes child state check
  or modification unnecessary difficult

This commit turns server part of the tool into a single
process spawning multiple children processes and herding
them via ZMQ client/server.

The parent process runs server part of the control
interface, maintains persistent VirtualBMC instances
configuration and ensures all its children are alive
and kicking. Each VirtualBMC instance is still a separate
parent fork.

If child dies, parent respawns it right away. If parent
is about to die, it tries its best to kill all the
prospective orphans.

This new implementation tries to stay compatible with
the original one in part of `vbmc` tool CLI interface
and behaviour. Whenever it can't connect to the `vbmcd`
it tries to fork and spawn the daemon behind the scenes.

While the threading design for this tool might look better,
the underlying pyghmi library is apparently rather
complicated to use its concurrency capabilities reliably.
The other minor consideration is that running multiple
processes leverages CPU-based concurrency.

Other changes:

* The `start` command now accepts more than one domains
  to be started

Change-Id: Ie10f4598c7039a7afa9b45d01df3b3c3db252c1d
Story: 1751570
Task:  12057
This commit is contained in:
Ilya Etingof 2017-07-28 12:00:56 +02:00
parent 189457ba9d
commit 7ace4293e9
19 changed files with 1192 additions and 324 deletions

View File

@ -1,6 +1,6 @@
=========== ==========
Virtual BMC VirtualBMC
=========== ==========
A virtual BMC for controlling virtual machines using IPMI commands. A virtual BMC for controlling virtual machines using IPMI commands.

View File

@ -6,9 +6,14 @@
Welcome to VirtualBMC's documentation! Welcome to VirtualBMC's documentation!
====================================== ======================================
A virtual BMC for controlling virtual machines using The VirtualBMC tool simulates a
`Baseboard Management Controller <https://en.wikipedia.org/wiki/Intelligent_Platform_Management_Interface#Baseboard_management_controller>`_
(BMC) by exposing
`IPMI <https://en.wikipedia.org/wiki/Intelligent_Platform_Management_Interface>`_ `IPMI <https://en.wikipedia.org/wiki/Intelligent_Platform_Management_Interface>`_
commands. responder to the network and talking to
`libvirt <https://en.wikipedia.org/wiki/Libvirt>`_
at the host vBMC is running at to manipulate virtual machines which pretend
to be bare metal servers.
Contents: Contents:

View File

@ -1,35 +1,52 @@
=====
Usage
=====
``vbmc`` is a CLI that lets users create, delete, list, start and stop How to use VirtualBMC
virtual BMCs for controlling virtual machines using IPMI commands. =====================
For the VirtualBMC tool to operate you first need to create libvirt
domain(s) for example, via ``virsh``. Or you can reuse any of the existing
domains if you do not mind bringing them up and down by way of
managing the simulated servers.
Command options The VirtualBMC tool is a client-server system where ``vbmcd`` server
--------------- does all the heavy-lifting (speaks IPMI, calls libvirt) while ``vbmc``
client is merely a command-line tool sending commands to the server and
rendering responses to the user.
In order to see all command options supporter by ``vbmc`` do:: You should set up your systemd to invoke the *vbmcd* server or you can
just run ``vbmcd`` from command line if you do not need the tool running
persistently on the system. Once the server is up and running, you can use
the ``vbmc`` tool to configure your libvirt domains as if they were physical
hardware servers.
By this moment you should be able to have the ``ipmitool`` managing
VirtualBMC instances over the network.
Configuring virtual servers
---------------------------
Use the ``vbmc`` command-line tool to create, delete, list, start and
stop virtual BMCs for the virtual machines being managed over IPMI.
* In order to see all command options supported by the ``vbmc`` tool
do::
$ vbmc --help $ vbmc --help
It's also possible to list the options from a specific command. For
example, in order to know what can be provided as part of the ``add`` It's also possible to list the options from a specific command. For
command do:: example, in order to know what can be provided as part of the ``add``
command do::
$ vbmc add --help $ vbmc add --help
Useful examples * Adding a new virtual BMC to control libvirt domain called ``node-0``::
---------------
* Adding a new virtual BMC to control a domain called ``node-0``::
$ vbmc add node-0 $ vbmc add node-0
* Adding a new virtual BMC to control a domain called ``node-1`` that * Adding a new virtual BMC to control libvirt domain called ``node-1``
will listen on the port ``6230``:: that will listen for IPMI commands on port ``6230``::
$ vbmc add node-1 --port 6230 $ vbmc add node-1 --port 6230
@ -39,17 +56,18 @@ Useful examples
with privilege will be able to start a virtual BMC on those ports. with privilege will be able to start a virtual BMC on those ports.
* Starting the virtual BMC to control the domain ``node-0``:: * Starting the virtual BMC to control libvirt domain ``node-0``::
$ vbmc start node-0 $ vbmc start node-0
* Stopping the virtual BMC that controls the domain ``node-0``:: * Stopping the virtual BMC that controls libvirt domain ``node-0``::
$ vbmc stop node-0 $ vbmc stop node-0
* Getting the list of virtual BMCs:: * Getting the list of virtual BMCs including their libvirt domains and
IPMI network endpoints they are reachable at::
$ vbmc list $ vbmc list
+-------------+---------+---------+------+ +-------------+---------+---------+------+
@ -59,8 +77,7 @@ Useful examples
| node-1 | running | :: | 6230 | | node-1 | running | :: | 6230 |
+-------------+---------+---------+------+ +-------------+---------+---------+------+
* To view configuration information for a specific virtual BMC::
* Showing the information of a specific virtual BMC::
$ vbmc show node-0 $ vbmc show node-0
+-----------------------+----------------+ +-----------------------+----------------+
@ -78,8 +95,8 @@ Useful examples
+-----------------------+----------------+ +-----------------------+----------------+
Testing Server simulation
------- -----------------
Once the virtual BMC for a specific domain has been created and started Once the virtual BMC for a specific domain has been created and started
you can then issue IPMI commands against the address and port of that you can then issue IPMI commands against the address and port of that
@ -100,3 +117,11 @@ virtual BMC to control the libvirt domain. For example:
* To get the current boot device:: * To get the current boot device::
$ ipmitool -I lanplus -U admin -P password -H 127.0.0.1 -p 6230 chassis bootparam get 5 $ ipmitool -I lanplus -U admin -P password -H 127.0.0.1 -p 6230 chassis bootparam get 5
Backward compatible behaviour
-----------------------------
In the past the ``vbmc`` tool was the only part of the vBMC system. To help
users keeping their existing server-less workflows, the ``vbmc`` tool
attempts to spawn the ``vbmcd`` piece whenever it figures server is not
running.

View File

@ -40,12 +40,13 @@ python-mimeparse==1.6.0
python-subunit==1.0.0 python-subunit==1.0.0
pytz==2013.6 pytz==2013.6
PyYAML==3.12 PyYAML==3.12
pyzmq===14.3.1
requests==2.14.2 requests==2.14.2
requestsexceptions==1.2.0 requestsexceptions==1.2.0
restructuredtext-lint==1.1.1 restructuredtext-lint==1.1.1
six==1.10.0 six==1.10.0
snowballstemmer==1.2.1 snowballstemmer==1.2.1
Sphinx==1.6.5 Sphinx==1.6.2
sphinxcontrib-websupport==1.0.1 sphinxcontrib-websupport==1.0.1
stestr==1.0.0 stestr==1.0.0
stevedore==1.20.0 stevedore==1.20.0

View File

@ -7,3 +7,4 @@ six>=1.10.0 # MIT
libvirt-python!=4.1.0,>=3.5.0 # LGPLv2+ libvirt-python!=4.1.0,>=3.5.0 # LGPLv2+
pyghmi>=1.0.22 # Apache-2.0 pyghmi>=1.0.22 # Apache-2.0
cliff!=2.9.0,>=2.8.0 # Apache-2.0 cliff!=2.9.0,>=2.8.0 # Apache-2.0
pyzmq>=14.3.1 # LGPL+BSD

View File

@ -25,6 +25,7 @@ packages =
[entry_points] [entry_points]
console_scripts = console_scripts =
vbmc = virtualbmc.cmd.vbmc:main vbmc = virtualbmc.cmd.vbmc:main
vbmcd = virtualbmc.cmd.vbmcd:main
virtualbmc = virtualbmc =
add = virtualbmc.cmd.vbmc:AddCommand add = virtualbmc.cmd.vbmc:AddCommand

View File

@ -10,17 +10,134 @@
# License for the specific language governing permissions and limitations # License for the specific language governing permissions and limitations
# under the License. # under the License.
import json
import logging import logging
import os
import sys import sys
import time
from cliff.app import App from cliff.app import App
from cliff.command import Command from cliff.command import Command
from cliff.commandmanager import CommandManager from cliff.commandmanager import CommandManager
from cliff.lister import Lister from cliff.lister import Lister
import zmq
import virtualbmc import virtualbmc
from virtualbmc import exception from virtualbmc.cmd import vbmcd
from virtualbmc.manager import VirtualBMCManager from virtualbmc import config as vbmc_config
from virtualbmc.exception import VirtualBMCError
from virtualbmc import log
CONF = vbmc_config.get_config()
LOG = log.get_logger()
class ZmqClient(object):
"""Client part of the VirtualBMC system.
The command-line client tool communicates with the server part
of the VirtualBMC system by exchanging JSON-encoded messages.
Client builds requests out of its command-line options which
include the command (e.g. `start`, `list` etc) and command-specific
options.
Server response is a JSON document which contains at least the
`rc` and `msg` attributes, used to indicate the outcome of the
command, and optionally 2-D table conveyed through the `header`
and `rows` attributes pointing to lists of cell values.
"""
SERVER_TIMEOUT = 5000 # milliseconds
def communicate(self, command, args, no_daemon=False):
data_out = {attr: getattr(args, attr)
for attr in dir(args) if not attr.startswith('_')}
data_out.update(command=command)
data_out = json.dumps(data_out)
server_port = CONF['default']['server_port']
context = socket = None
try:
context = zmq.Context()
socket = context.socket(zmq.REQ)
socket.setsockopt(zmq.LINGER, 5)
socket.connect("tcp://127.0.0.1:%s" % server_port)
poller = zmq.Poller()
poller.register(socket, zmq.POLLIN)
while True:
try:
if data_out:
socket.send(data_out.encode('utf-8'))
socks = dict(poller.poll(timeout=self.SERVER_TIMEOUT))
if socket in socks and socks[socket] == zmq.POLLIN:
data_in = socket.recv()
break
raise zmq.ZMQError('Server response timed out')
except zmq.ZMQError as ex:
LOG.debug('Server at %(port)s connection error: '
'%(error)s', {'port': server_port, 'error': ex})
if no_daemon:
msg = ('Server at %(port)s may be dead, will not '
'try to revive it' % {'port': server_port})
LOG.error(msg)
raise VirtualBMCError(msg)
no_daemon = True
LOG.debug("Attempting to start vBMC daemon behind the "
"scenes...")
LOG.debug("Please, configure your system to manage vbmcd "
"by systemd!")
# attempt to start and daemonize the server
if os.fork() == 0:
# this will also fork and detach properly
vbmcd.main([])
# TODO(etingof): perform some more retries
time.sleep(3)
# MQ will deliver the original message to the daemon
# we've started
data_out = {}
finally:
if socket:
socket.close()
context.destroy()
try:
data_in = json.loads(data_in.decode('utf-8'))
except ValueError as ex:
msg = 'Server response parsing error %(error)s' % {'error': ex}
LOG.error(msg)
raise VirtualBMCError(msg)
rc = data_in.pop('rc', None)
if rc:
msg = '(%(rc)s): %(msg)s' % {
'rc': rc,
'msg': '\n'.join(data_in.get('msg', ()))
}
LOG.error(msg)
raise VirtualBMCError(msg)
return data_in
class AddCommand(Command): class AddCommand(Command):
@ -78,14 +195,11 @@ class AddCommand(Command):
msg = ("A password and username are required to use " msg = ("A password and username are required to use "
"Libvirt's SASL authentication") "Libvirt's SASL authentication")
log.error(msg) log.error(msg)
raise exception.VirtualBMCError(msg) raise VirtualBMCError(msg)
self.app.manager.add(username=args.username, password=args.password, self.app.zmq.communicate(
port=args.port, address=args.address, 'add', args, no_daemon=self.app.options.no_daemon
domain_name=args.domain_name, )
libvirt_uri=args.libvirt_uri,
libvirt_sasl_username=sasl_user,
libvirt_sasl_password=sasl_pass)
class DeleteCommand(Command): class DeleteCommand(Command):
@ -100,8 +214,7 @@ class DeleteCommand(Command):
return parser return parser
def take_action(self, args): def take_action(self, args):
for domain in args.domain_names: self.app.zmq.communicate('delete', args, self.app.options.no_daemon)
self.app.manager.delete(domain)
class StartCommand(Command): class StartCommand(Command):
@ -116,7 +229,9 @@ class StartCommand(Command):
return parser return parser
def take_action(self, args): def take_action(self, args):
self.app.manager.start(args.domain_name) self.app.zmq.communicate(
'start', args, no_daemon=self.app.options.no_daemon
)
class StopCommand(Command): class StopCommand(Command):
@ -131,24 +246,19 @@ class StopCommand(Command):
return parser return parser
def take_action(self, args): def take_action(self, args):
for domain_name in args.domain_names: self.app.zmq.communicate(
self.app.manager.stop(domain_name) 'stop', args, no_daemon=self.app.options.no_daemon
)
class ListCommand(Lister): class ListCommand(Lister):
"""List all virtual BMC instances""" """List all virtual BMC instances"""
def take_action(self, args): def take_action(self, args):
header = ('Domain name', 'Status', 'Address', 'Port') rsp = self.app.zmq.communicate(
rows = [] 'list', args, no_daemon=self.app.options.no_daemon
)
for bmc in self.app.manager.list(): return rsp['header'], sorted(rsp['rows'])
rows.append(
([bmc['domain_name'], bmc['status'],
bmc['address'], bmc['port']])
)
return header, sorted(rows)
class ShowCommand(Lister): class ShowCommand(Lister):
@ -163,15 +273,10 @@ class ShowCommand(Lister):
return parser return parser
def take_action(self, args): def take_action(self, args):
header = ('Property', 'Value') rsp = self.app.zmq.communicate(
rows = [] 'show', args, no_daemon=self.app.options.no_daemon
)
bmc = self.app.manager.show(args.domain_name) return rsp['header'], sorted(rsp['rows'])
for key, val in bmc.items():
rows.append((key, val))
return header, sorted(rows)
class VirtualBMCApp(App): class VirtualBMCApp(App):
@ -185,8 +290,19 @@ class VirtualBMCApp(App):
deferred_help=True, deferred_help=True,
) )
def build_option_parser(self, description, version, argparse_kwargs=None):
parser = super(VirtualBMCApp, self).build_option_parser(
description, version, argparse_kwargs
)
parser.add_argument('--no-daemon',
action='store_true',
help='Do not start vbmcd automatically')
return parser
def initialize_app(self, argv): def initialize_app(self, argv):
self.manager = VirtualBMCManager() self.zmq = ZmqClient()
def clean_up(self, cmd, result, err): def clean_up(self, cmd, result, err):
self.LOG.debug('clean_up %s', cmd.__class__.__name__) self.LOG.debug('clean_up %s', cmd.__class__.__name__)

90
virtualbmc/cmd/vbmcd.py Normal file
View File

@ -0,0 +1,90 @@
# 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 os
import sys
import virtualbmc
from virtualbmc import config as vbmc_config
from virtualbmc import control
from virtualbmc import log
from virtualbmc import utils
LOG = log.get_logger()
CONF = vbmc_config.get_config()
def main(argv=sys.argv[1:]):
parser = argparse.ArgumentParser(
prog='VirtualBMC server',
description='A virtual BMC server for controlling virtual instances',
)
parser.add_argument('--version', action='version',
version=virtualbmc.__version__)
parser.add_argument('--foreground',
action='store_true',
default=False,
help='Do not daemonize')
args = parser.parse_args(argv)
pid_file = CONF['default']['pid_file']
try:
with open(pid_file) as f:
pid = int(f.read())
os.kill(pid, 0)
except Exception:
pass
else:
LOG.error('server PID #%(pid)d still running' % {'pid': pid})
return 1
def wrap_with_pidfile(func, pid):
dir_name = os.path.dirname(pid_file)
if not os.path.exists(dir_name):
os.makedirs(dir_name, mode=0o700)
with open(pid_file, 'w') as f:
f.write(str(pid))
try:
func()
except Exception as e:
LOG.error('%(error)s. ', {'error': e})
return 1
finally:
try:
os.unlink(pid_file)
except Exception:
pass
if args.foreground:
return wrap_with_pidfile(control.application, os.getpid())
else:
with utils.detach_process() as pid:
return wrap_with_pidfile(control.application, pid)
if __name__ == '__main__':
sys.exit(main())

View File

@ -35,7 +35,13 @@ class VirtualBMCConfig(object):
DEFAULTS = { DEFAULTS = {
'default': { 'default': {
'show_passwords': 'false', 'show_passwords': 'false',
'config_dir': os.path.join(os.path.expanduser('~'), '.vbmc'), 'config_dir': os.path.join(
os.path.expanduser('~'), '.vbmc'
),
'pid_file': os.path.join(
os.path.expanduser('~'), '.vbmc', 'master.pid'
),
'server_port': 50891,
}, },
'log': { 'log': {
'logfile': None, 'logfile': None,

217
virtualbmc/control.py Normal file
View File

@ -0,0 +1,217 @@
# Copyright 2017 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.
import json
import signal
import sys
import zmq
from virtualbmc import config as vbmc_config
from virtualbmc import exception
from virtualbmc import log
from virtualbmc.manager import VirtualBMCManager
CONF = vbmc_config.get_config()
LOG = log.get_logger()
TIMER_PERIOD = 3000 # milliseconds
def main_loop(vbmc_manager, handle_command):
"""Server part of the CLI control interface
Receives JSON messages from ZMQ socket, calls the command handler and
sends JSON response back to the client.
Client builds requests out of its command-line options which
include the command (e.g. `start`, `list` etc) and command-specific
options.
Server handles the commands and responds with a JSON document which
contains at least the `rc` and `msg` attributes, used to indicate the
outcome of the command, and optionally 2-D table conveyed through the
`header` and `rows` attributes pointing to lists of cell values.
"""
server_port = CONF['default']['server_port']
context = socket = None
try:
context = zmq.Context()
socket = context.socket(zmq.REP)
socket.setsockopt(zmq.LINGER, 5)
socket.bind("tcp://127.0.0.1:%s" % server_port)
poller = zmq.Poller()
poller.register(socket, zmq.POLLIN)
while True:
socks = dict(poller.poll(timeout=TIMER_PERIOD))
if socket in socks and socks[socket] == zmq.POLLIN:
message = socket.recv()
else:
vbmc_manager.periodic()
continue
try:
data_in = json.loads(message.decode('utf-8'))
except ValueError as ex:
LOG.warning(
'Control server request deserialization error: '
'%(error)s', {'error': ex}
)
continue
LOG.debug('Command request data: %(request)s', {'cmd': data_in})
try:
data_out = handle_command(vbmc_manager, data_in)
except exception.VirtualBMCError as ex:
msg = 'Command failed: %(error)s' % {'error': ex}
LOG.error(msg)
data_out = {
'rc': 1,
'msg': [msg]
}
LOG.debug('Command response data: %(response)s', {'cmd': data_out})
try:
message = json.dumps(data_out)
except ValueError as ex:
LOG.warning(
'Control server response serialization error: '
'%(error)s', {'error': ex}
)
continue
socket.send(message.encode('utf-8'))
finally:
if socket:
socket.close()
if context:
context.destroy()
def command_dispatcher(vbmc_manager, data_in):
"""Control CLI command dispatcher
Calls vBMC manager to execute commands, implements uniform
dictionary-based interface to the caller.
"""
command = data_in.pop('command')
LOG.debug('Running "%(cmd)s" command handler', {'cmd': command})
if command == 'add':
# Check if the username and password were given for SASL
sasl_user = data_in['libvirt_sasl_username']
sasl_pass = data_in['libvirt_sasl_password']
if any((sasl_user, sasl_pass)):
if not all((sasl_user, sasl_pass)):
error = ("A password and username are required to use "
"Libvirt's SASL authentication")
return {'msg': [error], 'rc': 1}
rc, msg = vbmc_manager.add(**data_in)
return {
'rc': rc,
'msg': [msg] if msg else []
}
elif command == 'delete':
data_out = [vbmc_manager.delete(domain_name)
for domain_name in data_in['domain_names']]
return {
'rc': max([rc for rc, msg in data_out]),
'msg': [msg for rc, msg in data_out if msg],
}
elif command == 'start':
data_out = [vbmc_manager.start(data_in['domain_name'])]
return {
'rc': max([rc for rc, msg in data_out]),
'msg': [msg for rc, msg in data_out if msg],
}
elif command == 'stop':
data_out = [vbmc_manager.stop(domain_name)
for domain_name in data_in['domain_names']]
return {
'rc': max([rc for rc, msg in data_out]),
'msg': [msg for rc, msg in data_out if msg],
}
elif command == 'list':
rc, tables = vbmc_manager.list()
header = ('Domain name', 'Status', 'Address', 'Port')
keys = ('domain_name', 'status', 'address', 'port')
return {
'rc': rc,
'header': header,
'rows': [
[table.get(key, '?') for key in keys] for table in tables
]
}
elif command == 'show':
rc, table = vbmc_manager.show(data_in['domain_name'])
return {
'rc': rc,
'header': ('Property', 'Value'),
'rows': table,
}
else:
return {
'rc': 1,
'msg': ['Unknown command'],
}
def application():
"""vbmcd application entry point
Initializes, serves and cleans up everything.
"""
vbmc_manager = VirtualBMCManager()
vbmc_manager.periodic()
def kill_children(*args):
vbmc_manager.periodic(shutdown=True)
sys.exit(0)
# SIGTERM does not seem to propagate to multiprocessing
signal.signal(signal.SIGTERM, kill_children)
try:
main_loop(vbmc_manager, command_dispatcher)
except Exception as ex:
LOG.error(
'Control server error: %(error)s', {'error': ex}
)
vbmc_manager.periodic(shutdown=True)

View File

@ -11,9 +11,9 @@
# under the License. # under the License.
import errno import errno
import multiprocessing
import os import os
import shutil import shutil
import signal
import six import six
from six.moves import configparser from six.moves import configparser
@ -37,56 +37,196 @@ CONF = vbmc_config.get_config()
class VirtualBMCManager(object): class VirtualBMCManager(object):
VBMC_OPTIONS = ['username', 'password', 'address', 'port',
'domain_name', 'libvirt_uri', 'libvirt_sasl_username',
'libvirt_sasl_password', 'active']
def __init__(self): def __init__(self):
super(VirtualBMCManager, self).__init__() super(VirtualBMCManager, self).__init__()
self.config_dir = CONF['default']['config_dir'] self.config_dir = CONF['default']['config_dir']
self._running_domains = {}
def _parse_config(self, domain_name): def _parse_config(self, domain_name):
config_path = os.path.join(self.config_dir, domain_name, 'config') config_path = os.path.join(self.config_dir, domain_name, 'config')
if not os.path.exists(config_path): if not os.path.exists(config_path):
raise exception.DomainNotFound(domain=domain_name) raise exception.DomainNotFound(domain=domain_name)
try:
config = configparser.ConfigParser()
config.read(config_path)
bmc = {}
for item in self.VBMC_OPTIONS:
try:
value = config.get(DEFAULT_SECTION, item)
except configparser.NoOptionError:
value = None
bmc[item] = value
# Port needs to be int
bmc['port'] = config.getint(DEFAULT_SECTION, 'port')
return bmc
except OSError:
raise exception.DomainNotFound(domain=domain_name)
def _store_config(self, **options):
config = configparser.ConfigParser() config = configparser.ConfigParser()
config.read(config_path) config.add_section(DEFAULT_SECTION)
for option, value in sorted(options.items()):
if value is not None:
config.set(DEFAULT_SECTION, option, six.text_type(value))
config_path = os.path.join(
self.config_dir, options['domain_name'], 'config'
)
with open(config_path, 'w') as f:
config.write(f)
def _vbmc_enabled(self, domain_name, lets_enable=None, config=None):
if not config:
config = self._parse_config(domain_name)
try:
currently_enabled = utils.str2bool(config['active'])
except Exception:
currently_enabled = False
if (lets_enable is not None and
lets_enable != currently_enabled):
config.update(active=lets_enable)
self._store_config(**config)
currently_enabled = lets_enable
return currently_enabled
def _sync_vbmc_states(self, shutdown=False):
"""Starts/stops vBMC instances
Walks over vBMC instances configuration, starts
enabled but dead instances, kills non-configured
but alive ones.
"""
def vbmc_runner(bmc_config):
show_passwords = CONF['default']['show_passwords']
if show_passwords:
show_options = bmc_config
else:
show_options = utils.mask_dict_password(bmc_config)
bmc = {}
for item in ('username', 'password', 'address', 'domain_name',
'libvirt_uri', 'libvirt_sasl_username',
'libvirt_sasl_password'):
try: try:
value = config.get(DEFAULT_SECTION, item) vbmc = VirtualBMC(**bmc_config)
except configparser.NoOptionError:
value = None
bmc[item] = value except Exception as ex:
LOG.error(
'Error running vBMC with configuration '
'%(opts)s: %(error)s' % {'opts': show_options,
'error': ex}
)
return
# Port needs to be int try:
bmc['port'] = config.getint(DEFAULT_SECTION, 'port') vbmc.listen(timeout=CONF['ipmi']['session_timeout'])
return bmc except Exception as ex:
LOG.info(
'Shutdown vBMC for domain %(domain)s, cause '
'%(error)s' % {'domain': show_options['domain_name'],
'error': ex}
)
return
for domain_name in os.listdir(self.config_dir):
if not os.path.isdir(
os.path.join(self.config_dir, domain_name)
):
continue
try:
bmc_config = self._parse_config(domain_name)
except exception.DomainNotFound:
continue
if shutdown:
lets_enable = False
else:
lets_enable = self._vbmc_enabled(
domain_name, config=bmc_config
)
instance = self._running_domains.get(domain_name)
if lets_enable:
if not instance:
instance = multiprocessing.Process(
name='xxx',
target=vbmc_runner,
args=(bmc_config,)
)
instance.daemon = True
instance.start()
self._running_domains[domain_name] = instance
LOG.info(
'Started vBMC instance for domain '
'%(domain)s' % {'domain': domain_name}
)
else:
if instance:
if instance.is_alive():
instance.terminate()
LOG.info(
'Terminated vBMC instance for domain '
'%(domain)s' % {'domain': domain_name}
)
if instance and not instance.is_alive():
del self._running_domains[domain_name]
LOG.info(
'Reaped vBMC instance for domain %(domain)s '
'(rc %(rc)s)' % {'domain': domain_name,
'rc': instance.exitcode}
)
def _show(self, domain_name): def _show(self, domain_name):
running = False
try:
pidfile_path = os.path.join(self.config_dir, domain_name, 'pid')
with open(pidfile_path, 'r') as f:
pid = int(f.read())
running = utils.is_pid_running(pid)
except (IOError, ValueError):
pass
bmc_config = self._parse_config(domain_name) bmc_config = self._parse_config(domain_name)
bmc_config['status'] = RUNNING if running else DOWN
# mask the passwords if requested show_passwords = CONF['default']['show_passwords']
if not CONF['default']['show_passwords']:
bmc_config = utils.mask_dict_password(bmc_config)
return bmc_config if show_passwords:
show_options = bmc_config
else:
show_options = utils.mask_dict_password(bmc_config)
def add(self, username, password, port, address, domain_name, libvirt_uri, instance = self._running_domains.get(domain_name)
libvirt_sasl_username, libvirt_sasl_password):
if instance and instance.is_alive():
show_options['status'] = RUNNING
else:
show_options['status'] = DOWN
return show_options
def periodic(self, shutdown=False):
self._sync_vbmc_states(shutdown)
def add(self, username, password, port, address, domain_name,
libvirt_uri, libvirt_sasl_username, libvirt_sasl_password,
**kwargs):
# check libvirt's connection and if domain exist prior to adding it # check libvirt's connection and if domain exist prior to adding it
utils.check_libvirt_connection_and_domain( utils.check_libvirt_connection_and_domain(
@ -95,33 +235,34 @@ class VirtualBMCManager(object):
sasl_password=libvirt_sasl_password) sasl_password=libvirt_sasl_password)
domain_path = os.path.join(self.config_dir, domain_name) domain_path = os.path.join(self.config_dir, domain_name)
try: try:
os.makedirs(domain_path) os.makedirs(domain_path)
except OSError as e: except OSError as ex:
if e.errno == errno.EEXIST: if ex.errno == errno.EEXIST:
raise exception.DomainAlreadyExists(domain=domain_name) return 1, str(ex)
raise exception.VirtualBMCError(
'Failed to create domain %(domain)s. Error: %(error)s' %
{'domain': domain_name, 'error': e})
config_path = os.path.join(domain_path, 'config') msg = ('Failed to create domain %(domain)s. '
with open(config_path, 'w') as f: 'Error: %(error)s' % {'domain': domain_name, 'error': ex})
config = configparser.ConfigParser() LOG.error(msg)
config.add_section(DEFAULT_SECTION) return 1, msg
config.set(DEFAULT_SECTION, 'username', username)
config.set(DEFAULT_SECTION, 'password', password)
config.set(DEFAULT_SECTION, 'port', six.text_type(port))
config.set(DEFAULT_SECTION, 'address', address)
config.set(DEFAULT_SECTION, 'domain_name', domain_name)
config.set(DEFAULT_SECTION, 'libvirt_uri', libvirt_uri)
if libvirt_sasl_username and libvirt_sasl_password: try:
config.set(DEFAULT_SECTION, 'libvirt_sasl_username', self._store_config(domain_name=domain_name,
libvirt_sasl_username) username=username,
config.set(DEFAULT_SECTION, 'libvirt_sasl_password', password=password,
libvirt_sasl_password) port=six.text_type(port),
address=address,
libvirt_uri=libvirt_uri,
libvirt_sasl_username=libvirt_sasl_username,
libvirt_sasl_password=libvirt_sasl_password,
active=False)
config.write(f) except Exception as ex:
self.delete(domain_name)
return 1, str(ex)
return 0, ''
def delete(self, domain_name): def delete(self, domain_name):
domain_path = os.path.join(self.config_dir, domain_name) domain_path = os.path.join(self.config_dir, domain_name)
@ -135,82 +276,56 @@ class VirtualBMCManager(object):
shutil.rmtree(domain_path) shutil.rmtree(domain_path)
return 0, ''
def start(self, domain_name): def start(self, domain_name):
domain_path = os.path.join(self.config_dir, domain_name) try:
if not os.path.exists(domain_path): bmc_config = self._parse_config(domain_name)
raise exception.DomainNotFound(domain=domain_name)
bmc_config = self._parse_config(domain_name) except Exception as ex:
return 1, str(ex)
# check libvirt's connection and domain prior to starting the BMC if domain_name in self._running_domains:
utils.check_libvirt_connection_and_domain( return 1, ('BMC instance %(domain)s '
bmc_config['libvirt_uri'], domain_name, 'already running' % {'domain': domain_name})
sasl_username=bmc_config['libvirt_sasl_username'],
sasl_password=bmc_config['libvirt_sasl_password'])
# mask the passwords if requested try:
log_config = bmc_config.copy() self._vbmc_enabled(domain_name,
if not CONF['default']['show_passwords']: config=bmc_config,
log_config = utils.mask_dict_password(bmc_config) lets_enable=True)
LOG.debug('Starting a Virtual BMC for domain %(domain)s with the ' except Exception as e:
'following configuration options: %(config)s', return 1, ('Failed to start domain %(domain)s. Error: '
{'domain': domain_name, '%(error)s' % {'domain': domain_name, 'error': e})
'config': ' '.join(['%s="%s"' % (k, log_config[k])
for k in log_config])})
with utils.detach_process() as pid_num: self._sync_vbmc_states()
try:
vbmc = VirtualBMC(**bmc_config)
except Exception as e:
msg = ('Error starting a Virtual BMC for domain %(domain)s. '
'Error: %(error)s' % {'domain': domain_name,
'error': e})
LOG.error(msg)
raise exception.VirtualBMCError(msg)
# Save the PID number return 0, ''
pidfile_path = os.path.join(domain_path, 'pid')
with open(pidfile_path, 'w') as f:
f.write(str(pid_num))
LOG.info('Virtual BMC for domain %s started', domain_name)
vbmc.listen(timeout=CONF['ipmi']['session_timeout'])
def stop(self, domain_name): def stop(self, domain_name):
LOG.debug('Stopping Virtual BMC for domain %s', domain_name)
domain_path = os.path.join(self.config_dir, 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: try:
with open(pidfile_path, 'r') as f: self._vbmc_enabled(domain_name, lets_enable=False)
pid = int(f.read())
except (IOError, ValueError):
raise exception.VirtualBMCError(
'Error stopping the domain %s: PID file not '
'found' % domain_name)
else:
os.remove(pidfile_path)
try: except Exception as ex:
os.kill(pid, signal.SIGKILL) return 1, str(ex)
except OSError:
pass self._sync_vbmc_states()
return 0, ''
def list(self): def list(self):
bmcs = [] rc = 0
tables = []
try: try:
for domain in os.listdir(self.config_dir): for domain in os.listdir(self.config_dir):
if os.path.isdir(os.path.join(self.config_dir, domain)): if os.path.isdir(os.path.join(self.config_dir, domain)):
bmcs.append(self._show(domain)) tables.append(self._show(domain))
except OSError as e: except OSError as e:
if e.errno == errno.EEXIST: if e.errno == errno.EEXIST:
return bmcs rc = 1
return bmcs return rc, tables
def show(self, domain_name): def show(self, domain_name):
return self._show(domain_name) return 0, list(self._show(domain_name).items())

View File

@ -14,7 +14,6 @@
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations # License for the specific language governing permissions and limitations
# under the License. # under the License.
from oslotest import base from oslotest import base

View File

@ -13,13 +13,14 @@
# License for the specific language governing permissions and limitations # License for the specific language governing permissions and limitations
# under the License. # under the License.
import json
import mock
import six import six
import sys import sys
import mock import zmq
from virtualbmc.cmd import vbmc from virtualbmc.cmd import vbmc
from virtualbmc import manager
from virtualbmc.tests.unit import base from virtualbmc.tests.unit import base
from virtualbmc.tests.unit import utils as test_utils from virtualbmc.tests.unit import utils as test_utils
@ -31,91 +32,259 @@ class VBMCTestCase(base.TestCase):
super(VBMCTestCase, self).setUp() super(VBMCTestCase, self).setUp()
self.domain = test_utils.get_domain() self.domain = test_utils.get_domain()
@mock.patch.object(manager.VirtualBMCManager, 'add') @mock.patch.object(zmq, 'Context')
def test_main_add(self, mock_add): @mock.patch.object(zmq, 'Poller')
argv = ['add'] def test_server_timeout(self, mock_zmq_poller, mock_zmq_context):
for option, value in self.domain.items(): expected_rc = 1
if option != 'domain_name': expected_output = ('Server at 50891 may be dead, '
argv.append('--' + option.replace('_', '-')) 'will not try to revive it\n')
argv.append(value and str(value))
argv.append(self.domain['domain_name'])
vbmc.main(argv)
mock_add.assert_called_once_with(**self.domain)
@mock.patch.object(manager.VirtualBMCManager, 'delete') mock_zmq_poller = mock_zmq_poller.return_value
def test_main_delete(self, mock_delete): mock_zmq_poller.poll.return_value = {}
argv = ['delete', 'foo', 'bar']
vbmc.main(argv)
expected_calls = [mock.call('foo'), mock.call('bar')]
self.assertEqual(expected_calls, mock_delete.call_args_list)
@mock.patch.object(manager.VirtualBMCManager, 'start') with mock.patch.object(sys, 'stderr', six.StringIO()) as output:
def test_main_start(self, mock_start): rc = vbmc.main(['--no-daemon',
argv = ['start', 'SpongeBob'] 'add', '--username', 'ironic', 'bar'])
vbmc.main(argv)
mock_start.assert_called_once_with('SpongeBob')
@mock.patch.object(manager.VirtualBMCManager, 'stop') self.assertEqual(expected_rc, rc)
def test_main_stop(self, mock_stop): self.assertEqual(expected_output, output.getvalue())
argv = ['stop', 'foo', 'bar']
vbmc.main(argv)
expected_calls = [mock.call('foo'), mock.call('bar')]
self.assertEqual(expected_calls, mock_stop.call_args_list)
@mock.patch.object(manager.VirtualBMCManager, 'list') @mock.patch.object(zmq, 'Context')
def test_main_list(self, mock_list): @mock.patch.object(zmq, 'Poller')
argv = ['list'] def test_main_add(self, mock_zmq_poller, mock_zmq_context):
expected_rc = 0
expected_output = ''
mock_list.return_value = [ srv_rsp = {
{'domain_name': 'node-1', 'rc': expected_rc,
'status': 'running', 'msg': ['OK']
'address': '::', }
'port': 321},
{'domain_name': 'node-0', mock_zmq_context = mock_zmq_context.return_value
'status': 'running', mock_zmq_socket = mock_zmq_context.socket.return_value
'address': '::', mock_zmq_socket.recv.return_value = json.dumps(srv_rsp).encode()
'port': 123}] mock_zmq_poller = mock_zmq_poller.return_value
mock_zmq_poller.poll.return_value = {
mock_zmq_socket: zmq.POLLIN
}
with mock.patch.object(sys, 'stdout', six.StringIO()) as output: with mock.patch.object(sys, 'stdout', six.StringIO()) as output:
vbmc.main(argv) rc = vbmc.main(['add', '--username', 'ironic', 'bar'])
out = output.getvalue()
expected_output = """\
+-------------+---------+---------+------+
| Domain name | Status | Address | Port |
+-------------+---------+---------+------+
| node-0 | running | :: | 123 |
| node-1 | running | :: | 321 |
+-------------+---------+---------+------+
"""
self.assertEqual(expected_output, out)
self.assertEqual(mock_list.call_count, 1) query = json.loads(mock_zmq_socket.send.call_args[0][0].decode())
@mock.patch.object(manager.VirtualBMCManager, 'show') expected_query = {
def test_main_show(self, mock_show): 'command': 'add',
argv = ['show', 'SpongeBob'] 'address': '::',
'port': 623,
'libvirt_uri': 'qemu:///system',
'libvirt_sasl_username': None,
'libvirt_sasl_password': None,
'username': 'ironic',
'password': 'password',
'domain_name': 'bar',
}
self.domain['status'] = 'running' self.assertEqual(expected_query, query)
mock_show.return_value = self.domain
self.assertEqual(expected_rc, rc)
self.assertEqual(expected_output, output.getvalue())
@mock.patch.object(zmq, 'Context')
@mock.patch.object(zmq, 'Poller')
def test_main_delete(self, mock_zmq_poller, mock_zmq_context):
expected_rc = 0
expected_output = ''
srv_rsp = {
'rc': expected_rc,
'msg': ['OK']
}
mock_zmq_context = mock_zmq_context.return_value
mock_zmq_socket = mock_zmq_context.socket.return_value
mock_zmq_socket.recv.return_value = json.dumps(srv_rsp).encode()
mock_zmq_poller = mock_zmq_poller.return_value
mock_zmq_poller.poll.return_value = {
mock_zmq_socket: zmq.POLLIN
}
with mock.patch.object(sys, 'stdout', six.StringIO()) as output: with mock.patch.object(sys, 'stdout', six.StringIO()) as output:
vbmc.main(argv)
out = output.getvalue()
expected_output = """\
+-----------------------+-----------+
| Property | Value |
+-----------------------+-----------+
| address | :: |
| domain_name | SpongeBob |
| libvirt_sasl_password | None |
| libvirt_sasl_username | None |
| libvirt_uri | foo://bar |
| password | pass |
| port | 123 |
| status | running |
| username | admin |
+-----------------------+-----------+
"""
self.assertEqual(expected_output, out)
self.assertEqual(mock_show.call_count, 1) rc = vbmc.main(['delete', 'foo', 'bar'])
query = json.loads(mock_zmq_socket.send.call_args[0][0].decode())
expected_query = {
"domain_names": ["foo", "bar"],
"command": "delete",
}
self.assertEqual(expected_query, query)
self.assertEqual(expected_rc, rc)
self.assertEqual(expected_output, output.getvalue())
@mock.patch.object(zmq, 'Context')
@mock.patch.object(zmq, 'Poller')
def test_main_start(self, mock_zmq_poller, mock_zmq_context):
expected_rc = 0
expected_output = ''
srv_rsp = {
'rc': expected_rc,
'msg': ['OK']
}
mock_zmq_context = mock_zmq_context.return_value
mock_zmq_socket = mock_zmq_context.socket.return_value
mock_zmq_socket.recv.return_value = json.dumps(srv_rsp).encode()
mock_zmq_poller = mock_zmq_poller.return_value
mock_zmq_poller.poll.return_value = {
mock_zmq_socket: zmq.POLLIN
}
with mock.patch.object(sys, 'stdout', six.StringIO()) as output:
rc = vbmc.main(['start', 'foo'])
query = json.loads(mock_zmq_socket.send.call_args[0][0].decode())
expected_query = {
'command': 'start',
'domain_name': 'foo'
}
self.assertEqual(expected_query, query)
self.assertEqual(expected_rc, rc)
self.assertEqual(expected_output, output.getvalue())
@mock.patch.object(zmq, 'Context')
@mock.patch.object(zmq, 'Poller')
def test_main_stop(self, mock_zmq_poller, mock_zmq_context):
expected_rc = 0
expected_output = ''
srv_rsp = {
'rc': expected_rc,
'msg': ['OK']
}
mock_zmq_context = mock_zmq_context.return_value
mock_zmq_socket = mock_zmq_context.socket.return_value
mock_zmq_socket.recv.return_value = json.dumps(srv_rsp).encode()
mock_zmq_poller = mock_zmq_poller.return_value
mock_zmq_poller.poll.return_value = {
mock_zmq_socket: zmq.POLLIN
}
with mock.patch.object(sys, 'stdout', six.StringIO()) as output:
rc = vbmc.main(['stop', 'foo', 'bar'])
query = json.loads(mock_zmq_socket.send.call_args[0][0].decode())
expected_query = {
'command': 'stop',
'domain_names': ['foo', 'bar']
}
self.assertEqual(expected_query, query)
self.assertEqual(expected_rc, rc)
self.assertEqual(expected_output, output.getvalue())
@mock.patch.object(zmq, 'Context')
@mock.patch.object(zmq, 'Poller')
def test_main_list(self, mock_zmq_poller, mock_zmq_context):
expected_rc = 0
expected_output = """+-------+-------+
| col1 | col2 |
+-------+-------+
| cell1 | cell2 |
| cell3 | cell4 |
+-------+-------+
"""
srv_rsp = {
'rc': expected_rc,
'header': ['col1', 'col2'],
'rows': [['cell1', 'cell2'],
['cell3', 'cell4']],
}
mock_zmq_context = mock_zmq_context.return_value
mock_zmq_socket = mock_zmq_context.socket.return_value
mock_zmq_socket.recv.return_value = json.dumps(srv_rsp).encode()
mock_zmq_poller = mock_zmq_poller.return_value
mock_zmq_poller.poll.return_value = {
mock_zmq_socket: zmq.POLLIN
}
with mock.patch.object(sys, 'stdout', six.StringIO()) as output:
rc = vbmc.main(['list'])
query = json.loads(mock_zmq_socket.send.call_args[0][0].decode())
expected_query = {
"command": "list",
}
# Cliff adds some extra args to the query
query = {key: query[key] for key in query
if key in expected_query}
self.assertEqual(expected_query, query)
self.assertEqual(expected_rc, rc)
self.assertEqual(expected_output, output.getvalue())
@mock.patch.object(zmq, 'Context')
@mock.patch.object(zmq, 'Poller')
def test_main_show(self, mock_zmq_poller, mock_zmq_context):
expected_rc = 0
expected_output = """+-------+-------+
| col1 | col2 |
+-------+-------+
| cell1 | cell2 |
| cell3 | cell4 |
+-------+-------+
"""
srv_rsp = {
'rc': expected_rc,
'header': ['col1', 'col2'],
'rows': [['cell1', 'cell2'],
['cell3', 'cell4']]
}
mock_zmq_context = mock_zmq_context.return_value
mock_zmq_socket = mock_zmq_context.socket.return_value
mock_zmq_socket.recv.return_value = json.dumps(srv_rsp).encode()
mock_zmq_poller = mock_zmq_poller.return_value
mock_zmq_poller.poll.return_value = {
mock_zmq_socket: zmq.POLLIN
}
with mock.patch.object(sys, 'stdout', six.StringIO()) as output:
rc = vbmc.main(['show', 'domain0'])
query = json.loads(mock_zmq_socket.send.call_args[0][0].decode())
expected_query = {
"domain_name": "domain0",
"command": "show",
}
# Cliff adds some extra args to the query
query = {key: query[key] for key in query
if key in expected_query}
self.assertEqual(expected_query, query)
self.assertEqual(expected_rc, rc)
self.assertEqual(expected_output, output.getvalue())

View File

@ -0,0 +1,51 @@
# Copyright 2017 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.
import os
import mock
from six.moves import builtins
from virtualbmc.cmd import vbmcd
from virtualbmc import control
from virtualbmc.tests.unit import base
from virtualbmc import utils
class VBMCDTestCase(base.TestCase):
@mock.patch.object(builtins, 'open')
@mock.patch.object(os, 'kill')
@mock.patch.object(os, 'unlink')
def test_main_foreground(self, mock_unlink, mock_kill, mock_open):
with mock.patch.object(control, 'application') as ml:
mock_kill.side_effect = OSError()
vbmcd.main(['--foreground'])
mock_kill.assert_called_once()
ml.assert_called_once()
mock_unlink.assert_called_once()
@mock.patch.object(builtins, 'open')
@mock.patch.object(os, 'kill')
@mock.patch.object(os, 'unlink')
def test_main_background(self, mock_unlink, mock_kill, mock_open):
with mock.patch.object(utils, 'detach_process') as dp, \
mock.patch.object(control, 'application') as ml:
mock_kill.side_effect = OSError()
vbmcd.main([])
mock_kill.assert_called_once()
dp.assert_called_once()
ml.assert_called_once()
mock_unlink.assert_called_once()

View File

@ -31,8 +31,10 @@ class VirtualBMCConfigTestCase(base.TestCase):
super(VirtualBMCConfigTestCase, self).setUp() super(VirtualBMCConfigTestCase, self).setUp()
self.vbmc_config = config.VirtualBMCConfig() self.vbmc_config = config.VirtualBMCConfig()
self.config_dict = {'default': {'show_passwords': 'true', self.config_dict = {'default': {'show_passwords': 'true',
'config_dir': '/foo'}, 'config_dir': '/foo/bar/1',
'log': {'debug': 'true', 'logfile': '/foo/bar'}, 'pid_file': '/foo/bar/2',
'server_port': '12345'},
'log': {'debug': 'true', 'logfile': '/foo/bar/4'},
'ipmi': {'session_timeout': '30'}} 'ipmi': {'session_timeout': '30'}}
@mock.patch.object(config.VirtualBMCConfig, '_validate') @mock.patch.object(config.VirtualBMCConfig, '_validate')
@ -53,8 +55,10 @@ class VirtualBMCConfigTestCase(base.TestCase):
config = mock.Mock() config = mock.Mock()
config.sections.side_effect = ['default', 'log', 'ipmi'], config.sections.side_effect = ['default', 'log', 'ipmi'],
config.items.side_effect = [[('show_passwords', 'true'), config.items.side_effect = [[('show_passwords', 'true'),
('config_dir', mock.ANY)], ('config_dir', '/foo/bar/1'),
[('logfile', '/foo/bar'), ('pid_file', '/foo/bar/2'),
('server_port', '12345')],
[('logfile', '/foo/bar/4'),
('debug', 'true')], ('debug', 'true')],
[('session_timeout', '30')]] [('session_timeout', '30')]]
ret = self.vbmc_config._as_dict(config) ret = self.vbmc_config._as_dict(config)

View File

@ -0,0 +1,72 @@
# Copyright 2017 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.
import json
import os
import mock
import zmq
from virtualbmc import control
from virtualbmc.tests.unit import base
class VBMCControlServerTestCase(base.TestCase):
@mock.patch.object(zmq, 'Context')
@mock.patch.object(zmq, 'Poller')
@mock.patch.object(os, 'path')
@mock.patch.object(os, 'remove')
def test_control_loop(self, mock_rm, mock_path, mock_zmq_poller,
mock_zmq_context):
mock_path.exists.return_value = False
mock_vbmc_manager = mock.MagicMock()
mock_handle_command = mock.MagicMock()
req = {
'command': 'list',
}
mock_zmq_context = mock_zmq_context.return_value
mock_zmq_socket = mock_zmq_context.socket.return_value
mock_zmq_socket.recv.return_value = json.dumps(req).encode()
mock_zmq_poller = mock_zmq_poller.return_value
mock_zmq_poller.poll.return_value = {
mock_zmq_socket: zmq.POLLIN
}
rsp = {
'rc': 0,
'msg': ['OK']
}
class QuitNow(Exception):
pass
mock_handle_command.return_value = rsp
mock_zmq_socket.send.side_effect = QuitNow()
self.assertRaises(QuitNow,
control.main_loop,
mock_vbmc_manager, mock_handle_command)
mock_zmq_socket.bind.assert_called_once()
mock_handle_command.assert_called_once()
response = json.loads(mock_zmq_socket.send.call_args[0][0].decode())
self.assertEqual(rsp, response)

View File

@ -15,9 +15,9 @@
import copy import copy
import errno import errno
import multiprocessing
import os import os
import shutil import shutil
import signal
import mock import mock
from six.moves import builtins from six.moves import builtins
@ -49,7 +49,8 @@ class VirtualBMCManagerTestCase(base.TestCase):
'domain_name': 'Squidward Tentacles', 'domain_name': 'Squidward Tentacles',
'libvirt_uri': 'foo://bar', 'libvirt_uri': 'foo://bar',
'libvirt_sasl_username': 'sasl_admin', 'libvirt_sasl_username': 'sasl_admin',
'libvirt_sasl_password': 'sasl_pass'} 'libvirt_sasl_password': 'sasl_pass',
'active': 'False'}
def _get_config(self, section, item): def _get_config(self, section, item):
return self.domain0.get(item) return self.domain0.get(item)
@ -69,9 +70,10 @@ class VirtualBMCManagerTestCase(base.TestCase):
expected_get_calls = [mock.call('VirtualBMC', i) expected_get_calls = [mock.call('VirtualBMC', i)
for i in ('username', 'password', 'address', for i in ('username', 'password', 'address',
'domain_name', 'libvirt_uri', 'port', 'domain_name', 'libvirt_uri',
'libvirt_sasl_username', 'libvirt_sasl_username',
'libvirt_sasl_password')] 'libvirt_sasl_password',
'active')]
self.assertEqual(expected_get_calls, config.get.call_args_list) self.assertEqual(expected_get_calls, config.get.call_args_list)
@mock.patch.object(os.path, 'exists') @mock.patch.object(os.path, 'exists')
@ -82,10 +84,8 @@ class VirtualBMCManagerTestCase(base.TestCase):
mock_exists.assert_called_once_with(self.domain_path0 + '/config') mock_exists.assert_called_once_with(self.domain_path0 + '/config')
@mock.patch.object(builtins, 'open') @mock.patch.object(builtins, 'open')
@mock.patch.object(utils, 'is_pid_running')
@mock.patch.object(manager.VirtualBMCManager, '_parse_config') @mock.patch.object(manager.VirtualBMCManager, '_parse_config')
def _test__show(self, mock__parse, mock_pid, mock_open, expected=None): def _test__show(self, mock__parse, mock_open, expected=None):
mock_pid.return_value = True
mock__parse.return_value = self.domain0 mock__parse.return_value = self.domain0
f = mock.MagicMock() f = mock.MagicMock()
f.read.return_value = self.domain0['port'] f.read.return_value = self.domain0['port']
@ -93,7 +93,7 @@ class VirtualBMCManagerTestCase(base.TestCase):
if expected is None: if expected is None:
expected = self.domain0.copy() expected = self.domain0.copy()
expected['status'] = manager.RUNNING expected['status'] = manager.DOWN
ret = self.manager._show(self.domain_name0) ret = self.manager._show(self.domain_name0)
self.assertEqual(expected, ret) self.assertEqual(expected, ret)
@ -109,7 +109,7 @@ class VirtualBMCManagerTestCase(base.TestCase):
expected = self.domain0.copy() expected = self.domain0.copy()
expected['password'] = '***' expected['password'] = '***'
expected['libvirt_sasl_password'] = '***' expected['libvirt_sasl_password'] = '***'
expected['status'] = manager.RUNNING expected['status'] = manager.DOWN
self._test__show(expected=expected) self._test__show(expected=expected)
@mock.patch.object(builtins, 'open') @mock.patch.object(builtins, 'open')
@ -168,8 +168,12 @@ class VirtualBMCManagerTestCase(base.TestCase):
os_error.errno = errno.EEXIST os_error.errno = errno.EEXIST
mock_makedirs.side_effect = os_error mock_makedirs.side_effect = os_error
self.assertRaises(exception.DomainAlreadyExists, ret, _ = self.manager.add(**self.add_params)
self.manager.add, **self.add_params)
expected_ret = 1
self.assertEqual(ret, expected_ret)
mock_check_conn.assert_called_once_with( mock_check_conn.assert_called_once_with(
self.add_params['libvirt_uri'], self.add_params['domain_name'], self.add_params['libvirt_uri'], self.add_params['domain_name'],
sasl_username=self.add_params['libvirt_sasl_username'], sasl_username=self.add_params['libvirt_sasl_username'],
@ -180,8 +184,10 @@ class VirtualBMCManagerTestCase(base.TestCase):
def test_add_oserror(self, mock_check_conn, mock_makedirs): def test_add_oserror(self, mock_check_conn, mock_makedirs):
mock_makedirs.side_effect = OSError mock_makedirs.side_effect = OSError
self.assertRaises(exception.VirtualBMCError, ret, _ = self.manager.add(**self.add_params)
self.manager.add, **self.add_params) expected_ret = 1
self.assertEqual(ret, expected_ret)
mock_check_conn.assert_called_once_with( mock_check_conn.assert_called_once_with(
self.add_params['libvirt_uri'], self.add_params['domain_name'], self.add_params['libvirt_uri'], self.add_params['domain_name'],
sasl_username=self.add_params['libvirt_sasl_username'], sasl_username=self.add_params['libvirt_sasl_username'],
@ -206,67 +212,55 @@ class VirtualBMCManagerTestCase(base.TestCase):
mock_exists.assert_called_once_with(self.domain_path0) mock_exists.assert_called_once_with(self.domain_path0)
@mock.patch.object(builtins, 'open') @mock.patch.object(builtins, 'open')
@mock.patch.object(manager, 'VirtualBMC')
@mock.patch.object(utils, 'detach_process')
@mock.patch.object(utils, 'check_libvirt_connection_and_domain')
@mock.patch.object(manager.VirtualBMCManager, '_parse_config') @mock.patch.object(manager.VirtualBMCManager, '_parse_config')
@mock.patch.object(os.path, 'exists') @mock.patch.object(os.path, 'exists')
def test_start(self, mock_exists, mock__parse, mock_check_conn, @mock.patch.object(os.path, 'isdir')
mock_detach, mock_vbmc, mock_open): @mock.patch.object(os, 'listdir')
@mock.patch.object(multiprocessing, 'Process')
def test_start(self, mock_process, mock_listdir, mock_isdir, mock_exists,
mock__parse, mock_open):
conf = {'ipmi': {'session_timeout': 10}, conf = {'ipmi': {'session_timeout': 10},
'default': {'show_passwords': False}} 'default': {'show_passwords': False}}
with mock.patch('virtualbmc.manager.CONF', conf): with mock.patch('virtualbmc.manager.CONF', conf):
mock_listdir.return_value = [self.domain_name0]
mock_isdir.return_value = True
mock_exists.return_value = True mock_exists.return_value = True
mock__parse.return_value = self.domain0 domain0_conf = self.domain0.copy()
mock_detach.return_value.__enter__.return_value = 99999 domain0_conf.update(active='False')
mock__parse.return_value = domain0_conf
file_handler = mock_open.return_value.__enter__.return_value file_handler = mock_open.return_value.__enter__.return_value
self.manager.start(self.domain_name0) self.manager.start(self.domain_name0)
mock__parse.assert_called_with(self.domain_name0)
mock_exists.assert_called_once_with(self.domain_path0) self.assertEqual(file_handler.write.call_count, 9)
mock__parse.assert_called_once_with(self.domain_name0)
mock_check_conn.assert_called_once_with(
self.domain0['libvirt_uri'], self.domain0['domain_name'],
sasl_username=self.domain0['libvirt_sasl_username'],
sasl_password=self.domain0['libvirt_sasl_password'])
mock_detach.assert_called_once_with()
mock_vbmc.assert_called_once_with(**self.domain0)
mock_vbmc.return_value.listen.assert_called_once_with(timeout=10)
file_handler.write.assert_called_once_with('99999')
@mock.patch.object(builtins, 'open') @mock.patch.object(builtins, 'open')
@mock.patch.object(os, 'kill') @mock.patch.object(manager.VirtualBMCManager, '_parse_config')
@mock.patch.object(os, 'remove') @mock.patch.object(os.path, 'isdir')
@mock.patch.object(os.path, 'exists') @mock.patch.object(os, 'listdir')
def test_stop(self, mock_exists, mock_remove, mock_kill, mock_open): def test_stop(self, mock_listdir, mock_isdir, mock__parse, mock_open):
mock_exists.return_value = True conf = {'ipmi': {'session_timeout': 10},
f = mock.MagicMock() 'default': {'show_passwords': False}}
f.read.return_value = self.domain0['port'] with mock.patch('virtualbmc.manager.CONF', conf):
mock_open.return_value.__enter__.return_value = f mock_listdir.return_value = [self.domain_name0]
mock_isdir.return_value = True
self.manager.stop(self.domain_name0) domain0_conf = self.domain0.copy()
f.read.assert_called_once_with() domain0_conf.update(active='True')
mock_exists.assert_called_once_with(self.domain_path0) mock__parse.return_value = domain0_conf
mock_remove.assert_called_once_with(self.domain_path0 + '/pid') file_handler = mock_open.return_value.__enter__.return_value
mock_kill.assert_called_once_with(self.domain0['port'], self.manager.stop(self.domain_name0)
signal.SIGKILL) mock_isdir.assert_called_once_with(self.domain_path0)
mock__parse.assert_called_with(self.domain_name0)
self.assertEqual(file_handler.write.call_count, 9)
@mock.patch.object(os.path, 'exists') @mock.patch.object(os.path, 'exists')
def test_stop_domain_not_found(self, mock_exists): def test_stop_domain_not_found(self, mock_exists):
mock_exists.return_value = False mock_exists.return_value = False
self.assertRaises(exception.DomainNotFound, ret = self.manager.stop(self.domain_name0)
self.manager.stop, self.domain_name0) expected_ret = 1, 'No domain with matching name SpongeBob was found'
mock_exists.assert_called_once_with(self.domain_path0) self.assertEqual(ret, expected_ret)
mock_exists.assert_called_once_with(
@mock.patch.object(builtins, 'open') os.path.join(self.domain_path0, 'config')
@mock.patch.object(os.path, 'exists') )
def test_stop_pid_file_not_found(self, mock_exists, mock_open):
mock_exists.return_value = True
f = mock.MagicMock()
f.read.return_value = self.domain0['port']
mock_open.return_value.__enter__.side_effect = IOError('boom')
self.assertRaises(exception.VirtualBMCError,
self.manager.stop, self.domain_name0)
@mock.patch.object(os.path, 'isdir') @mock.patch.object(os.path, 'isdir')
@mock.patch.object(os, 'listdir') @mock.patch.object(os, 'listdir')
@ -274,16 +268,17 @@ class VirtualBMCManagerTestCase(base.TestCase):
def test_list(self, mock__show, mock_listdir, mock_isdir): def test_list(self, mock__show, mock_listdir, mock_isdir):
mock_isdir.return_value = True mock_isdir.return_value = True
mock_listdir.return_value = (self.domain_name0, self.domain_name1) mock_listdir.return_value = (self.domain_name0, self.domain_name1)
expected_ret = [self.domain0, self.domain1]
mock__show.side_effect = expected_ret
ret = self.manager.list()
self.assertEqual(expected_ret, ret) ret, _ = self.manager.list()
expected_ret = 0
self.assertEqual(ret, expected_ret)
mock_listdir.assert_called_once_with(_CONFIG_PATH) mock_listdir.assert_called_once_with(_CONFIG_PATH)
expected_calls = [mock.call(self.domain_path0), expected_calls = [mock.call(self.domain_path0),
mock.call(self.domain_path1)] mock.call(self.domain_path1)]
self.assertEqual(expected_calls, mock_isdir.call_args_list) self.assertEqual(expected_calls, mock_isdir.call_args_list)
expected_calls = [mock.call(self.domain_name0),
mock.call(self.domain_name1)]
self.assertEqual(expected_calls, mock__show.call_args_list)
@mock.patch.object(manager.VirtualBMCManager, '_show') @mock.patch.object(manager.VirtualBMCManager, '_show')
def test_show(self, mock__show): def test_show(self, mock__show):

View File

@ -22,7 +22,8 @@ def get_domain(**kwargs):
'password': kwargs.get('password', 'pass'), 'password': kwargs.get('password', 'pass'),
'libvirt_uri': kwargs.get('libvirt_uri', 'foo://bar'), 'libvirt_uri': kwargs.get('libvirt_uri', 'foo://bar'),
'libvirt_sasl_username': kwargs.get('libvirt_sasl_username'), 'libvirt_sasl_username': kwargs.get('libvirt_sasl_username'),
'libvirt_sasl_password': kwargs.get('libvirt_sasl_password')} 'libvirt_sasl_password': kwargs.get('libvirt_sasl_password'),
'active': kwargs.get('active', False)}
status = kwargs.get('status') status = kwargs.get('status')
if status is not None: if status is not None:

View File

@ -51,7 +51,7 @@ class VirtualBMC(bmc.Bmc):
def __init__(self, username, password, port, address, def __init__(self, username, password, port, address,
domain_name, libvirt_uri, libvirt_sasl_username=None, domain_name, libvirt_uri, libvirt_sasl_username=None,
libvirt_sasl_password=None): libvirt_sasl_password=None, **kwargs):
super(VirtualBMC, self).__init__({username: password}, super(VirtualBMC, self).__init__({username: password},
port=port, address=address) port=port, address=address)
self.domain_name = domain_name self.domain_name = domain_name