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.

View File

@ -6,9 +6,14 @@
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>`_
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:

View File

@ -1,35 +1,52 @@
=====
Usage
=====
``vbmc`` is a CLI that lets users create, delete, list, start and stop
virtual BMCs for controlling virtual machines using IPMI commands.
How to use VirtualBMC
=====================
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
It's also possible to list the options from a specific command. For
example, in order to know what can be provided as part of the ``add``
command do::
It's also possible to list the options from a specific command. For
example, in order to know what can be provided as part of the ``add``
command do::
$ vbmc add --help
Useful examples
---------------
* Adding a new virtual BMC to control a domain called ``node-0``::
* Adding a new virtual BMC to control libvirt domain called ``node-0``::
$ vbmc add node-0
* Adding a new virtual BMC to control a domain called ``node-1`` that
will listen on the port ``6230``::
* Adding a new virtual BMC to control libvirt domain called ``node-1``
that will listen for IPMI commands on 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.
* Starting the virtual BMC to control the domain ``node-0``::
* Starting the virtual BMC to control libvirt domain ``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
* 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
+-------------+---------+---------+------+
@ -59,8 +77,7 @@ Useful examples
| node-1 | running | :: | 6230 |
+-------------+---------+---------+------+
* Showing the information of a specific virtual BMC::
* To view configuration information for a specific virtual BMC::
$ 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
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::
$ 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
pytz==2013.6
PyYAML==3.12
pyzmq===14.3.1
requests==2.14.2
requestsexceptions==1.2.0
restructuredtext-lint==1.1.1
six==1.10.0
snowballstemmer==1.2.1
Sphinx==1.6.5
Sphinx==1.6.2
sphinxcontrib-websupport==1.0.1
stestr==1.0.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+
pyghmi>=1.0.22 # 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]
console_scripts =
vbmc = virtualbmc.cmd.vbmc:main
vbmcd = virtualbmc.cmd.vbmcd:main
virtualbmc =
add = virtualbmc.cmd.vbmc:AddCommand

View File

@ -10,17 +10,134 @@
# License for the specific language governing permissions and limitations
# under the License.
import json
import logging
import os
import sys
import time
from cliff.app import App
from cliff.command import Command
from cliff.commandmanager import CommandManager
from cliff.lister import Lister
import zmq
import virtualbmc
from virtualbmc import exception
from virtualbmc.manager import VirtualBMCManager
from virtualbmc.cmd import vbmcd
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):
@ -78,14 +195,11 @@ class AddCommand(Command):
msg = ("A password and username are required to use "
"Libvirt's SASL authentication")
log.error(msg)
raise exception.VirtualBMCError(msg)
raise VirtualBMCError(msg)
self.app.manager.add(username=args.username, password=args.password,
port=args.port, address=args.address,
domain_name=args.domain_name,
libvirt_uri=args.libvirt_uri,
libvirt_sasl_username=sasl_user,
libvirt_sasl_password=sasl_pass)
self.app.zmq.communicate(
'add', args, no_daemon=self.app.options.no_daemon
)
class DeleteCommand(Command):
@ -100,8 +214,7 @@ class DeleteCommand(Command):
return parser
def take_action(self, args):
for domain in args.domain_names:
self.app.manager.delete(domain)
self.app.zmq.communicate('delete', args, self.app.options.no_daemon)
class StartCommand(Command):
@ -116,7 +229,9 @@ class StartCommand(Command):
return parser
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):
@ -131,24 +246,19 @@ class StopCommand(Command):
return parser
def take_action(self, args):
for domain_name in args.domain_names:
self.app.manager.stop(domain_name)
self.app.zmq.communicate(
'stop', args, no_daemon=self.app.options.no_daemon
)
class ListCommand(Lister):
"""List all virtual BMC instances"""
def take_action(self, args):
header = ('Domain name', 'Status', 'Address', 'Port')
rows = []
for bmc in self.app.manager.list():
rows.append(
([bmc['domain_name'], bmc['status'],
bmc['address'], bmc['port']])
)
return header, sorted(rows)
rsp = self.app.zmq.communicate(
'list', args, no_daemon=self.app.options.no_daemon
)
return rsp['header'], sorted(rsp['rows'])
class ShowCommand(Lister):
@ -163,15 +273,10 @@ class ShowCommand(Lister):
return parser
def take_action(self, args):
header = ('Property', 'Value')
rows = []
bmc = self.app.manager.show(args.domain_name)
for key, val in bmc.items():
rows.append((key, val))
return header, sorted(rows)
rsp = self.app.zmq.communicate(
'show', args, no_daemon=self.app.options.no_daemon
)
return rsp['header'], sorted(rsp['rows'])
class VirtualBMCApp(App):
@ -185,8 +290,19 @@ class VirtualBMCApp(App):
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):
self.manager = VirtualBMCManager()
self.zmq = ZmqClient()
def clean_up(self, cmd, result, err):
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 = {
'default': {
'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': {
'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.
import errno
import multiprocessing
import os
import shutil
import signal
import six
from six.moves import configparser
@ -37,56 +37,196 @@ CONF = vbmc_config.get_config()
class VirtualBMCManager(object):
VBMC_OPTIONS = ['username', 'password', 'address', 'port',
'domain_name', 'libvirt_uri', 'libvirt_sasl_username',
'libvirt_sasl_password', 'active']
def __init__(self):
super(VirtualBMCManager, self).__init__()
self.config_dir = CONF['default']['config_dir']
self._running_domains = {}
def _parse_config(self, domain_name):
config_path = os.path.join(self.config_dir, domain_name, 'config')
if not os.path.exists(config_path):
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.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:
value = config.get(DEFAULT_SECTION, item)
except configparser.NoOptionError:
value = None
vbmc = VirtualBMC(**bmc_config)
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
bmc['port'] = config.getint(DEFAULT_SECTION, 'port')
try:
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):
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['status'] = RUNNING if running else DOWN
# mask the passwords if requested
if not CONF['default']['show_passwords']:
bmc_config = utils.mask_dict_password(bmc_config)
show_passwords = CONF['default']['show_passwords']
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,
libvirt_sasl_username, libvirt_sasl_password):
instance = self._running_domains.get(domain_name)
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
utils.check_libvirt_connection_and_domain(
@ -95,33 +235,34 @@ class VirtualBMCManager(object):
sasl_password=libvirt_sasl_password)
domain_path = os.path.join(self.config_dir, domain_name)
try:
os.makedirs(domain_path)
except OSError as e:
if e.errno == errno.EEXIST:
raise exception.DomainAlreadyExists(domain=domain_name)
raise exception.VirtualBMCError(
'Failed to create domain %(domain)s. Error: %(error)s' %
{'domain': domain_name, 'error': e})
except OSError as ex:
if ex.errno == errno.EEXIST:
return 1, str(ex)
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', 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)
msg = ('Failed to create domain %(domain)s. '
'Error: %(error)s' % {'domain': domain_name, 'error': ex})
LOG.error(msg)
return 1, msg
if libvirt_sasl_username and libvirt_sasl_password:
config.set(DEFAULT_SECTION, 'libvirt_sasl_username',
libvirt_sasl_username)
config.set(DEFAULT_SECTION, 'libvirt_sasl_password',
libvirt_sasl_password)
try:
self._store_config(domain_name=domain_name,
username=username,
password=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):
domain_path = os.path.join(self.config_dir, domain_name)
@ -135,82 +276,56 @@ class VirtualBMCManager(object):
shutil.rmtree(domain_path)
return 0, ''
def start(self, 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)
try:
bmc_config = self._parse_config(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
utils.check_libvirt_connection_and_domain(
bmc_config['libvirt_uri'], domain_name,
sasl_username=bmc_config['libvirt_sasl_username'],
sasl_password=bmc_config['libvirt_sasl_password'])
if domain_name in self._running_domains:
return 1, ('BMC instance %(domain)s '
'already running' % {'domain': domain_name})
# mask the passwords if requested
log_config = bmc_config.copy()
if not CONF['default']['show_passwords']:
log_config = utils.mask_dict_password(bmc_config)
try:
self._vbmc_enabled(domain_name,
config=bmc_config,
lets_enable=True)
LOG.debug('Starting a Virtual BMC for domain %(domain)s with the '
'following configuration options: %(config)s',
{'domain': domain_name,
'config': ' '.join(['%s="%s"' % (k, log_config[k])
for k in log_config])})
except Exception as e:
return 1, ('Failed to start domain %(domain)s. Error: '
'%(error)s' % {'domain': domain_name, 'error': e})
with utils.detach_process() as pid_num:
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)
self._sync_vbmc_states()
# Save the PID number
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'])
return 0, ''
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:
with open(pidfile_path, 'r') as f:
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)
self._vbmc_enabled(domain_name, lets_enable=False)
try:
os.kill(pid, signal.SIGKILL)
except OSError:
pass
except Exception as ex:
return 1, str(ex)
self._sync_vbmc_states()
return 0, ''
def list(self):
bmcs = []
rc = 0
tables = []
try:
for domain in os.listdir(self.config_dir):
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:
if e.errno == errno.EEXIST:
return bmcs
rc = 1
return bmcs
return rc, tables
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
# License for the specific language governing permissions and limitations
# under the License.
from oslotest import base

View File

@ -13,13 +13,14 @@
# License for the specific language governing permissions and limitations
# under the License.
import json
import mock
import six
import sys
import mock
import zmq
from virtualbmc.cmd import vbmc
from virtualbmc import manager
from virtualbmc.tests.unit import base
from virtualbmc.tests.unit import utils as test_utils
@ -31,91 +32,259 @@ class VBMCTestCase(base.TestCase):
super(VBMCTestCase, self).setUp()
self.domain = test_utils.get_domain()
@mock.patch.object(manager.VirtualBMCManager, 'add')
def test_main_add(self, mock_add):
argv = ['add']
for option, value in self.domain.items():
if option != 'domain_name':
argv.append('--' + option.replace('_', '-'))
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(zmq, 'Context')
@mock.patch.object(zmq, 'Poller')
def test_server_timeout(self, mock_zmq_poller, mock_zmq_context):
expected_rc = 1
expected_output = ('Server at 50891 may be dead, '
'will not try to revive it\n')
@mock.patch.object(manager.VirtualBMCManager, 'delete')
def test_main_delete(self, mock_delete):
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_zmq_poller = mock_zmq_poller.return_value
mock_zmq_poller.poll.return_value = {}
@mock.patch.object(manager.VirtualBMCManager, 'start')
def test_main_start(self, mock_start):
argv = ['start', 'SpongeBob']
vbmc.main(argv)
mock_start.assert_called_once_with('SpongeBob')
with mock.patch.object(sys, 'stderr', six.StringIO()) as output:
rc = vbmc.main(['--no-daemon',
'add', '--username', 'ironic', 'bar'])
@mock.patch.object(manager.VirtualBMCManager, 'stop')
def test_main_stop(self, mock_stop):
argv = ['stop', 'foo', 'bar']
vbmc.main(argv)
expected_calls = [mock.call('foo'), mock.call('bar')]
self.assertEqual(expected_calls, mock_stop.call_args_list)
self.assertEqual(expected_rc, rc)
self.assertEqual(expected_output, output.getvalue())
@mock.patch.object(manager.VirtualBMCManager, 'list')
def test_main_list(self, mock_list):
argv = ['list']
@mock.patch.object(zmq, 'Context')
@mock.patch.object(zmq, 'Poller')
def test_main_add(self, mock_zmq_poller, mock_zmq_context):
expected_rc = 0
expected_output = ''
mock_list.return_value = [
{'domain_name': 'node-1',
'status': 'running',
'address': '::',
'port': 321},
{'domain_name': 'node-0',
'status': 'running',
'address': '::',
'port': 123}]
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:
vbmc.main(argv)
out = output.getvalue()
expected_output = """\
+-------------+---------+---------+------+
| Domain name | Status | Address | Port |
+-------------+---------+---------+------+
| node-0 | running | :: | 123 |
| node-1 | running | :: | 321 |
+-------------+---------+---------+------+
"""
self.assertEqual(expected_output, out)
rc = vbmc.main(['add', '--username', 'ironic', 'bar'])
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')
def test_main_show(self, mock_show):
argv = ['show', 'SpongeBob']
expected_query = {
'command': 'add',
'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'
mock_show.return_value = self.domain
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_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:
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