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:
parent
189457ba9d
commit
7ace4293e9
@ -1,6 +1,6 @@
|
||||
===========
|
||||
Virtual BMC
|
||||
===========
|
||||
==========
|
||||
VirtualBMC
|
||||
==========
|
||||
|
||||
A virtual BMC for controlling virtual machines using IPMI commands.
|
||||
|
||||
|
@ -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:
|
||||
|
||||
|
@ -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.
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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
90
virtualbmc/cmd/vbmcd.py
Normal 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())
|
@ -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
217
virtualbmc/control.py
Normal 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)
|
@ -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())
|
||||
|
@ -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
|
||||
|
||||
|
||||
|
@ -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 |