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.
|
A virtual BMC for controlling virtual machines using IPMI commands.
|
||||||
|
|
||||||
|
@ -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:
|
||||||
|
|
||||||
|
@ -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.
|
||||||
|
@ -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
|
||||||
|
@ -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
|
||||||
|
@ -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
|
||||||
|
@ -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
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 = {
|
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
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.
|
# 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())
|
||||||
|
@ -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
|
||||||
|
|
||||||
|
|
||||||
|
@ -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())
|
||||||
|
51
virtualbmc/tests/unit/cmd/test_vbmcd.py
Normal file
51
virtualbmc/tests/unit/cmd/test_vbmcd.py
Normal 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()
|
@ -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)
|
||||||
|
72
virtualbmc/tests/unit/test_control.py
Normal file
72
virtualbmc/tests/unit/test_control.py
Normal 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)
|
@ -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):
|
||||||
|
@ -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:
|
||||||
|
@ -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
|
||||||
|
Loading…
Reference in New Issue
Block a user