320 lines
10 KiB
Python
320 lines
10 KiB
Python
# 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 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.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 = CONF['default']['server_response_timeout']
|
|
|
|
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(CONF['default']['server_spawn_wait'] / 1000.)
|
|
|
|
# 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):
|
|
"""Create a new BMC for a virtual machine instance"""
|
|
|
|
def get_parser(self, prog_name):
|
|
parser = super(AddCommand, self).get_parser(prog_name)
|
|
|
|
parser.add_argument('domain_name',
|
|
help='The name of the virtual machine')
|
|
parser.add_argument('--username',
|
|
dest='username',
|
|
default='admin',
|
|
help='The BMC username; defaults to "admin"')
|
|
parser.add_argument('--password',
|
|
dest='password',
|
|
default='password',
|
|
help='The BMC password; defaults to "password"')
|
|
parser.add_argument('--port',
|
|
dest='port',
|
|
type=int,
|
|
default=623,
|
|
help='Port to listen on; defaults to 623')
|
|
parser.add_argument('--address',
|
|
dest='address',
|
|
default='::',
|
|
help=('The address to bind to (IPv4 and IPv6 '
|
|
'are supported); defaults to ::'))
|
|
parser.add_argument('--libvirt-uri',
|
|
dest='libvirt_uri',
|
|
default="qemu:///system",
|
|
help=('The libvirt URI; defaults to '
|
|
'"qemu:///system"'))
|
|
parser.add_argument('--libvirt-sasl-username',
|
|
dest='libvirt_sasl_username',
|
|
default=None,
|
|
help=('The libvirt SASL username; defaults to '
|
|
'None'))
|
|
parser.add_argument('--libvirt-sasl-password',
|
|
dest='libvirt_sasl_password',
|
|
default=None,
|
|
help=('The libvirt SASL password; defaults to '
|
|
'None'))
|
|
return parser
|
|
|
|
def take_action(self, args):
|
|
|
|
log = logging.getLogger(__name__)
|
|
|
|
# Check if the username and password were given for SASL
|
|
sasl_user = args.libvirt_sasl_username
|
|
sasl_pass = args.libvirt_sasl_password
|
|
if any((sasl_user, sasl_pass)):
|
|
if not all((sasl_user, sasl_pass)):
|
|
msg = ("A password and username are required to use "
|
|
"Libvirt's SASL authentication")
|
|
log.error(msg)
|
|
raise VirtualBMCError(msg)
|
|
|
|
self.app.zmq.communicate(
|
|
'add', args, no_daemon=self.app.options.no_daemon
|
|
)
|
|
|
|
|
|
class DeleteCommand(Command):
|
|
"""Delete a virtual BMC for a virtual machine instance"""
|
|
|
|
def get_parser(self, prog_name):
|
|
parser = super(DeleteCommand, self).get_parser(prog_name)
|
|
|
|
parser.add_argument('domain_names', nargs='+',
|
|
help='A list of virtual machine names')
|
|
|
|
return parser
|
|
|
|
def take_action(self, args):
|
|
self.app.zmq.communicate('delete', args, self.app.options.no_daemon)
|
|
|
|
|
|
class StartCommand(Command):
|
|
"""Start a virtual BMC for a virtual machine instance"""
|
|
|
|
def get_parser(self, prog_name):
|
|
parser = super(StartCommand, self).get_parser(prog_name)
|
|
|
|
parser.add_argument('domain_name',
|
|
help='The name of the virtual machine')
|
|
|
|
return parser
|
|
|
|
def take_action(self, args):
|
|
self.app.zmq.communicate(
|
|
'start', args, no_daemon=self.app.options.no_daemon
|
|
)
|
|
|
|
|
|
class StopCommand(Command):
|
|
"""Stop a virtual BMC for a virtual machine instance"""
|
|
|
|
def get_parser(self, prog_name):
|
|
parser = super(StopCommand, self).get_parser(prog_name)
|
|
|
|
parser.add_argument('domain_names', nargs='+',
|
|
help='A list of virtual machine names')
|
|
|
|
return parser
|
|
|
|
def take_action(self, args):
|
|
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):
|
|
rsp = self.app.zmq.communicate(
|
|
'list', args, no_daemon=self.app.options.no_daemon
|
|
)
|
|
return rsp['header'], sorted(rsp['rows'])
|
|
|
|
|
|
class ShowCommand(Lister):
|
|
"""Show virtual BMC properties"""
|
|
|
|
def get_parser(self, prog_name):
|
|
parser = super(ShowCommand, self).get_parser(prog_name)
|
|
|
|
parser.add_argument('domain_name',
|
|
help='The name of the virtual machine')
|
|
|
|
return parser
|
|
|
|
def take_action(self, args):
|
|
rsp = self.app.zmq.communicate(
|
|
'show', args, no_daemon=self.app.options.no_daemon
|
|
)
|
|
return rsp['header'], sorted(rsp['rows'])
|
|
|
|
|
|
class VirtualBMCApp(App):
|
|
|
|
def __init__(self):
|
|
super(VirtualBMCApp, self).__init__(
|
|
description='Virtual Baseboard Management Controller (BMC) backed '
|
|
'by virtual machines',
|
|
version=virtualbmc.__version__,
|
|
command_manager=CommandManager('virtualbmc'),
|
|
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.zmq = ZmqClient()
|
|
|
|
def clean_up(self, cmd, result, err):
|
|
self.LOG.debug('clean_up %s', cmd.__class__.__name__)
|
|
if err:
|
|
self.LOG.debug('got an error: %s', err)
|
|
|
|
|
|
def main(argv=sys.argv[1:]):
|
|
vbmc_app = VirtualBMCApp()
|
|
return vbmc_app.run(argv)
|
|
|
|
|
|
if __name__ == '__main__':
|
|
sys.exit(main())
|