diff --git a/README.rst b/README.rst index 2f14f53..969dcdc 100644 --- a/README.rst +++ b/README.rst @@ -1,6 +1,6 @@ -=========== -Virtual BMC -=========== +========== +VirtualBMC +========== A virtual BMC for controlling virtual machines using IPMI commands. diff --git a/doc/source/index.rst b/doc/source/index.rst index 909a139..445609b 100644 --- a/doc/source/index.rst +++ b/doc/source/index.rst @@ -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 `_ +(BMC) by exposing `IPMI `_ -commands. +responder to the network and talking to +`libvirt `_ +at the host vBMC is running at to manipulate virtual machines which pretend +to be bare metal servers. Contents: diff --git a/doc/source/user/index.rst b/doc/source/user/index.rst index 0931f64..d91a76a 100644 --- a/doc/source/user/index.rst +++ b/doc/source/user/index.rst @@ -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. diff --git a/lower-constraints.txt b/lower-constraints.txt index 7a91f31..e32d223 100644 --- a/lower-constraints.txt +++ b/lower-constraints.txt @@ -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 diff --git a/requirements.txt b/requirements.txt index 3e567d6..a63b982 100644 --- a/requirements.txt +++ b/requirements.txt @@ -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 diff --git a/setup.cfg b/setup.cfg index 8563d25..b217be3 100644 --- a/setup.cfg +++ b/setup.cfg @@ -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 diff --git a/virtualbmc/cmd/vbmc.py b/virtualbmc/cmd/vbmc.py index cc0e33b..304177c 100644 --- a/virtualbmc/cmd/vbmc.py +++ b/virtualbmc/cmd/vbmc.py @@ -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__) diff --git a/virtualbmc/cmd/vbmcd.py b/virtualbmc/cmd/vbmcd.py new file mode 100644 index 0000000..1540f5e --- /dev/null +++ b/virtualbmc/cmd/vbmcd.py @@ -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()) diff --git a/virtualbmc/config.py b/virtualbmc/config.py index e96fef0..4b73a48 100644 --- a/virtualbmc/config.py +++ b/virtualbmc/config.py @@ -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, diff --git a/virtualbmc/control.py b/virtualbmc/control.py new file mode 100644 index 0000000..13ca418 --- /dev/null +++ b/virtualbmc/control.py @@ -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) diff --git a/virtualbmc/manager.py b/virtualbmc/manager.py index ed4c86b..b65e02d 100644 --- a/virtualbmc/manager.py +++ b/virtualbmc/manager.py @@ -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()) diff --git a/virtualbmc/tests/unit/base.py b/virtualbmc/tests/unit/base.py index 1c30cdb..1f435bd 100644 --- a/virtualbmc/tests/unit/base.py +++ b/virtualbmc/tests/unit/base.py @@ -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 diff --git a/virtualbmc/tests/unit/cmd/test_vbmc.py b/virtualbmc/tests/unit/cmd/test_vbmc.py index 2f045a0..c6e2ae7 100644 --- a/virtualbmc/tests/unit/cmd/test_vbmc.py +++ b/virtualbmc/tests/unit/cmd/test_vbmc.py @@ -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': ['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()) diff --git a/virtualbmc/tests/unit/cmd/test_vbmcd.py b/virtualbmc/tests/unit/cmd/test_vbmcd.py new file mode 100644 index 0000000..01ba557 --- /dev/null +++ b/virtualbmc/tests/unit/cmd/test_vbmcd.py @@ -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() diff --git a/virtualbmc/tests/unit/test_config.py b/virtualbmc/tests/unit/test_config.py index 035b4ce..43ad746 100644 --- a/virtualbmc/tests/unit/test_config.py +++ b/virtualbmc/tests/unit/test_config.py @@ -31,8 +31,10 @@ class VirtualBMCConfigTestCase(base.TestCase): super(VirtualBMCConfigTestCase, self).setUp() self.vbmc_config = config.VirtualBMCConfig() self.config_dict = {'default': {'show_passwords': 'true', - 'config_dir': '/foo'}, - 'log': {'debug': 'true', 'logfile': '/foo/bar'}, + 'config_dir': '/foo/bar/1', + 'pid_file': '/foo/bar/2', + 'server_port': '12345'}, + 'log': {'debug': 'true', 'logfile': '/foo/bar/4'}, 'ipmi': {'session_timeout': '30'}} @mock.patch.object(config.VirtualBMCConfig, '_validate') @@ -53,8 +55,10 @@ class VirtualBMCConfigTestCase(base.TestCase): config = mock.Mock() config.sections.side_effect = ['default', 'log', 'ipmi'], config.items.side_effect = [[('show_passwords', 'true'), - ('config_dir', mock.ANY)], - [('logfile', '/foo/bar'), + ('config_dir', '/foo/bar/1'), + ('pid_file', '/foo/bar/2'), + ('server_port', '12345')], + [('logfile', '/foo/bar/4'), ('debug', 'true')], [('session_timeout', '30')]] ret = self.vbmc_config._as_dict(config) diff --git a/virtualbmc/tests/unit/test_control.py b/virtualbmc/tests/unit/test_control.py new file mode 100644 index 0000000..50932b6 --- /dev/null +++ b/virtualbmc/tests/unit/test_control.py @@ -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) diff --git a/virtualbmc/tests/unit/test_manager.py b/virtualbmc/tests/unit/test_manager.py index ec28c1a..f7dbaac 100644 --- a/virtualbmc/tests/unit/test_manager.py +++ b/virtualbmc/tests/unit/test_manager.py @@ -15,9 +15,9 @@ import copy import errno +import multiprocessing import os import shutil -import signal import mock from six.moves import builtins @@ -49,7 +49,8 @@ class VirtualBMCManagerTestCase(base.TestCase): 'domain_name': 'Squidward Tentacles', 'libvirt_uri': 'foo://bar', 'libvirt_sasl_username': 'sasl_admin', - 'libvirt_sasl_password': 'sasl_pass'} + 'libvirt_sasl_password': 'sasl_pass', + 'active': 'False'} def _get_config(self, section, item): return self.domain0.get(item) @@ -69,9 +70,10 @@ class VirtualBMCManagerTestCase(base.TestCase): expected_get_calls = [mock.call('VirtualBMC', i) for i in ('username', 'password', 'address', - 'domain_name', 'libvirt_uri', + 'port', 'domain_name', 'libvirt_uri', 'libvirt_sasl_username', - 'libvirt_sasl_password')] + 'libvirt_sasl_password', + 'active')] self.assertEqual(expected_get_calls, config.get.call_args_list) @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.patch.object(builtins, 'open') - @mock.patch.object(utils, 'is_pid_running') @mock.patch.object(manager.VirtualBMCManager, '_parse_config') - def _test__show(self, mock__parse, mock_pid, mock_open, expected=None): - mock_pid.return_value = True + def _test__show(self, mock__parse, mock_open, expected=None): mock__parse.return_value = self.domain0 f = mock.MagicMock() f.read.return_value = self.domain0['port'] @@ -93,7 +93,7 @@ class VirtualBMCManagerTestCase(base.TestCase): if expected is None: expected = self.domain0.copy() - expected['status'] = manager.RUNNING + expected['status'] = manager.DOWN ret = self.manager._show(self.domain_name0) self.assertEqual(expected, ret) @@ -109,7 +109,7 @@ class VirtualBMCManagerTestCase(base.TestCase): expected = self.domain0.copy() expected['password'] = '***' expected['libvirt_sasl_password'] = '***' - expected['status'] = manager.RUNNING + expected['status'] = manager.DOWN self._test__show(expected=expected) @mock.patch.object(builtins, 'open') @@ -168,8 +168,12 @@ class VirtualBMCManagerTestCase(base.TestCase): os_error.errno = errno.EEXIST mock_makedirs.side_effect = os_error - self.assertRaises(exception.DomainAlreadyExists, - self.manager.add, **self.add_params) + ret, _ = self.manager.add(**self.add_params) + + expected_ret = 1 + + self.assertEqual(ret, expected_ret) + mock_check_conn.assert_called_once_with( self.add_params['libvirt_uri'], self.add_params['domain_name'], 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): mock_makedirs.side_effect = OSError - self.assertRaises(exception.VirtualBMCError, - self.manager.add, **self.add_params) + ret, _ = self.manager.add(**self.add_params) + expected_ret = 1 + self.assertEqual(ret, expected_ret) + mock_check_conn.assert_called_once_with( self.add_params['libvirt_uri'], self.add_params['domain_name'], 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.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(os.path, 'exists') - def test_start(self, mock_exists, mock__parse, mock_check_conn, - mock_detach, mock_vbmc, mock_open): + @mock.patch.object(os.path, 'isdir') + @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}, 'default': {'show_passwords': False}} 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__parse.return_value = self.domain0 - mock_detach.return_value.__enter__.return_value = 99999 + domain0_conf = self.domain0.copy() + domain0_conf.update(active='False') + mock__parse.return_value = domain0_conf file_handler = mock_open.return_value.__enter__.return_value self.manager.start(self.domain_name0) - - mock_exists.assert_called_once_with(self.domain_path0) - 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__parse.assert_called_with(self.domain_name0) + self.assertEqual(file_handler.write.call_count, 9) @mock.patch.object(builtins, 'open') - @mock.patch.object(os, 'kill') - @mock.patch.object(os, 'remove') - @mock.patch.object(os.path, 'exists') - def test_stop(self, mock_exists, mock_remove, mock_kill, mock_open): - mock_exists.return_value = True - f = mock.MagicMock() - f.read.return_value = self.domain0['port'] - mock_open.return_value.__enter__.return_value = f - - self.manager.stop(self.domain_name0) - f.read.assert_called_once_with() - mock_exists.assert_called_once_with(self.domain_path0) - mock_remove.assert_called_once_with(self.domain_path0 + '/pid') - mock_kill.assert_called_once_with(self.domain0['port'], - signal.SIGKILL) + @mock.patch.object(manager.VirtualBMCManager, '_parse_config') + @mock.patch.object(os.path, 'isdir') + @mock.patch.object(os, 'listdir') + def test_stop(self, mock_listdir, mock_isdir, mock__parse, mock_open): + conf = {'ipmi': {'session_timeout': 10}, + 'default': {'show_passwords': False}} + with mock.patch('virtualbmc.manager.CONF', conf): + mock_listdir.return_value = [self.domain_name0] + mock_isdir.return_value = True + domain0_conf = self.domain0.copy() + domain0_conf.update(active='True') + mock__parse.return_value = domain0_conf + file_handler = mock_open.return_value.__enter__.return_value + self.manager.stop(self.domain_name0) + 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') def test_stop_domain_not_found(self, mock_exists): mock_exists.return_value = False - self.assertRaises(exception.DomainNotFound, - self.manager.stop, self.domain_name0) - mock_exists.assert_called_once_with(self.domain_path0) - - @mock.patch.object(builtins, 'open') - @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) + ret = self.manager.stop(self.domain_name0) + expected_ret = 1, 'No domain with matching name SpongeBob was found' + self.assertEqual(ret, expected_ret) + mock_exists.assert_called_once_with( + os.path.join(self.domain_path0, 'config') + ) @mock.patch.object(os.path, 'isdir') @mock.patch.object(os, 'listdir') @@ -274,16 +268,17 @@ class VirtualBMCManagerTestCase(base.TestCase): def test_list(self, mock__show, mock_listdir, mock_isdir): mock_isdir.return_value = True 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) - expected_calls = [mock.call(self.domain_path0), mock.call(self.domain_path1)] 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') def test_show(self, mock__show): diff --git a/virtualbmc/tests/unit/utils.py b/virtualbmc/tests/unit/utils.py index a881960..9d385a4 100644 --- a/virtualbmc/tests/unit/utils.py +++ b/virtualbmc/tests/unit/utils.py @@ -22,7 +22,8 @@ def get_domain(**kwargs): 'password': kwargs.get('password', 'pass'), 'libvirt_uri': kwargs.get('libvirt_uri', 'foo://bar'), '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') if status is not None: diff --git a/virtualbmc/vbmc.py b/virtualbmc/vbmc.py index c20a9f1..d0ab380 100644 --- a/virtualbmc/vbmc.py +++ b/virtualbmc/vbmc.py @@ -51,7 +51,7 @@ class VirtualBMC(bmc.Bmc): def __init__(self, username, password, port, address, domain_name, libvirt_uri, libvirt_sasl_username=None, - libvirt_sasl_password=None): + libvirt_sasl_password=None, **kwargs): super(VirtualBMC, self).__init__({username: password}, port=port, address=address) self.domain_name = domain_name