X-Git-Url: https://gerrit.akraino.org/r/gitweb?p=ta%2Fironic-virtmedia-driver.git;a=blobdiff_plain;f=src%2Fironic_virtmedia_driver%2Fvirtmedia_ssh_boot.py;fp=src%2Fironic_virtmedia_driver%2Fvirtmedia_ssh_boot.py;h=d29d3939d6afcb266e3ac44f781464f7e3d680b7;hp=0000000000000000000000000000000000000000;hb=be2fb9459b65bb8b07b23adebadc16530d26e231;hpb=aaec6775b13b0c694235c80e6dfbca8696307af9 diff --git a/src/ironic_virtmedia_driver/virtmedia_ssh_boot.py b/src/ironic_virtmedia_driver/virtmedia_ssh_boot.py new file mode 100644 index 0000000..d29d393 --- /dev/null +++ b/src/ironic_virtmedia_driver/virtmedia_ssh_boot.py @@ -0,0 +1,272 @@ +# Copyright 2019 Nokia +# +# 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 + +from oslo_concurrency import processutils +from oslo_log import log as logging +from oslo_utils import strutils + +import retrying +import paramiko +import six + +from ironic.common import exception +from ironic.common import utils +from ironic.common.i18n import _, _translators +from ironic.drivers import utils as driver_utils +from ironic.conf import CONF +from ironic_virtmedia_driver import virtmedia + +LOG = logging.getLogger(__name__) + +REQUIRED_PROPERTIES = { + 'ssh_address': _("IP address of the node to ssh into from where the VMs can be managed. " + "Required."), + 'ssh_username': _("username to authenticate as. Required."), + 'ssh_key_contents': _("private key(s). If ssh_password is also specified " + "it will be used for unlocking the private key. Do " + "not specify ssh_key_filename when this property is " + "specified.") +} + +COMMON_PROPERTIES = REQUIRED_PROPERTIES.copy() +CONSOLE_PROPERTIES = { + 'ssh_terminal_port': _("node's UDP port to connect to. Only required for " + "console access and only applicable for 'virsh'.") +} + +def _get_command_sets(): + """Retrieves the virt_type-specific commands. + Returns commands are as follows: + + base_cmd: Used by most sub-commands as the primary executable + list_all: Lists all VMs (by virt_type identifier) that can be managed. + One name per line, must not be quoted. + get_disk_list: Gets the list of Block devices connected to the VM + attach_disk_device: Attaches a given disk device to VM + detach_disk_device: Detaches a disk device from a VM + """ + virt_type="virsh" + if virt_type == "virsh": + virsh_cmds = { + 'base_cmd': 'LC_ALL=C /usr/bin/virsh', + 'list_all': 'list --all --name', + 'get_disk_list': ( + "domblklist --domain {_NodeName_} | " + "grep var | awk -F \" \" '{print $1}'"), + 'attach_disk_device': 'attach-disk --domain {_NodeName_} --source /var/lib/libvirt/images/{_ImageName_} --target {_TargetDev_} --sourcetype file --mode readonly --type {_DevType_} --config', + 'detach_disk_device': 'detach-disk --domain {_NodeName_} --target {_TargetDev_} --config', + } + + return virsh_cmds + else: + raise exception.InvalidParameterValue(_( + "SSHPowerDriver '%(virt_type)s' is not a valid virt_type, ") % + {'virt_type': virt_type}) + +def _ssh_execute(ssh_obj, cmd_to_exec): + """Executes a command via ssh. + + Executes a command via ssh and returns a list of the lines of the + output from the command. + + :param ssh_obj: paramiko.SSHClient, an active ssh connection. + :param cmd_to_exec: command to execute. + :returns: list of the lines of output from the command. + :raises: SSHCommandFailed on an error from ssh. + + """ + LOG.debug(_translators.log_error("Executing SSH cmd: %r"), cmd_to_exec) + try: + output_list = processutils.ssh_execute(ssh_obj, + cmd_to_exec)[0].split('\n') + except Exception as e: + LOG.error(_translators.log_error("Cannot execute SSH cmd %(cmd)s. Reason: %(err)s."), + {'cmd': cmd_to_exec, 'err': e}) + raise exception.SSHCommandFailed(cmd=cmd_to_exec) + + return output_list + + +def _parse_driver_info(node): + """Gets the information needed for accessing the node. + + :param node: the Node of interest. + :returns: dictionary of information. + :raises: InvalidParameterValue if any required parameters are incorrect. + :raises: MissingParameterValue if any required parameters are missing. + + """ + info = node.driver_info or {} + missing_info = [key for key in REQUIRED_PROPERTIES if not info.get(key)] + if missing_info: + raise exception.MissingParameterValue(_( + "SSHPowerDriver requires the following parameters to be set in " + "node's driver_info: %s.") % missing_info) + + address = info.get('ssh_address') + username = info.get('ssh_username') + key_contents = info.get('ssh_key_contents') + terminal_port = info.get('ssh_terminal_port') + + if terminal_port is not None: + terminal_port = utils.validate_network_port(terminal_port, + 'ssh_terminal_port') + + # NOTE(deva): we map 'address' from API to 'host' for common utils + res = { + 'host': address, + 'username': username, + 'port': 22, + 'uuid': node.uuid, + 'terminal_port': terminal_port + } + + cmd_set = _get_command_sets() + res['cmd_set'] = cmd_set + + if key_contents: + res['key_contents'] = key_contents + else: + raise exception.InvalidParameterValue(_( + "ssh_virtmedia Driver requires ssh_key_contents to be set.")) + return res + +def _get_ssh_connection(connection): + """Returns an SSH client connected to a node. + + :param node: the Node. + :returns: paramiko.SSHClient, an active ssh connection. + + """ + try: + ssh = paramiko.SSHClient() + ssh.set_missing_host_key_policy(paramiko.AutoAddPolicy()) + key_contents = connection.get('key_contents') + if key_contents: + data = six.StringIO(key_contents) + if "BEGIN RSA PRIVATE" in key_contents: + pkey = paramiko.RSAKey.from_private_key(data) + elif "BEGIN DSA PRIVATE" in key_contents: + pkey = paramiko.DSSKey.from_private_key(data) + else: + # Can't include the key contents - secure material. + raise ValueError(_("Invalid private key")) + else: + pkey = None + ssh.connect(connection.get('host'), + username=connection.get('username'), + password=None, + port=connection.get('port', 22), + pkey=pkey, + key_filename=connection.get('key_filename'), + timeout=connection.get('timeout', 10)) + + # send TCP keepalive packets every 20 seconds + ssh.get_transport().set_keepalive(20) + except Exception as e: + LOG.debug("SSH connect failed: %s", e) + raise exception.SSHConnectFailed(host=connection.get('host')) + + return ssh + +def _get_disk_attachment_status(driver_info, node_name, ssh_obj, target_disk='hda'): + cmd_to_exec = "%s %s" % (driver_info['cmd_set']['base_cmd'], + driver_info['cmd_set']['get_disk_list']) + cmd_to_exec = cmd_to_exec.replace('{_NodeName_}', node_name) + blk_dev_list = _ssh_execute(ssh_obj, cmd_to_exec) + LOG.debug("Attached block devices list for node %(node_name)s: %(blk_dev_list)s", {'node_name': node_name, 'blk_dev_list': blk_dev_list}) + if target_disk in blk_dev_list: + return True + else: + return False + +def _get_sftp_connection(connection): + try: + key_contents = connection.get('key_contents') + if key_contents: + data = six.StringIO(key_contents) + if "BEGIN RSA PRIVATE" in key_contents: + pkey = paramiko.RSAKey.from_private_key(data) + elif "BEGIN DSA PRIVATE" in key_contents: + pkey = paramiko.DSSKey.from_private_key(data) + else: + # Can't include the key contents - secure material. + raise ValueError(_("Invalid private key")) + else: + pkey = None + + sftp_obj = paramiko.Transport((connection.get('host'), connection.get('port', 22))) + sftp_obj.connect(None, username=connection.get('username'), + password=None, pkey=pkey) + return sftp_obj + except Exception as e: + LOG.error(_translators.log_error("Cannot establish connection to sftp target. Reason: %(err)s."), + {'err': e}) + raise exception.CommunicationError(e) + +def _copy_media_to_virt_server(sftp_obj, media_file): + LOG.debug("Copying file: %s to target" %(media_file)) + sftp = paramiko.SFTPClient.from_transport(sftp_obj) + try: + sftp.put('/remote_image_share_root/%s' %media_file, '/var/lib/libvirt/images/%s' %media_file) + except Exception as e: + LOG.error(_translators.log_error("Cannot copy %(media_file)s to target. Reason: %(err)s."), + {'media_file': media_file, 'err': e}) + raise exception.CommunicationError(media_file) + +class VirtualMediaAndSSHBoot(virtmedia.VirtmediaBoot): + def __init__(self): + """Constructor of VirtualMediaAndSSHBoot. + + :raises: InvalidParameterValue, if config option has invalid value. + """ + super(VirtualMediaAndSSHBoot, self).__init__() + + def _attach_virtual_cd(self, task, image_filename): + driver_info = _parse_driver_info(task.node) + ssh_obj = _get_ssh_connection(driver_info) + sftp_obj = _get_sftp_connection(driver_info) + node_name = task.node.name + if _get_disk_attachment_status(driver_info, node_name, ssh_obj, 'hda'): + LOG.debug("A CD is already attached to node %s, not taking any action", node_name) + return + + _copy_media_to_virt_server(sftp_obj, image_filename) + cmd_to_exec = "%s %s" % (driver_info['cmd_set']['base_cmd'], + driver_info['cmd_set']['attach_disk_device']) + cmd_to_exec = cmd_to_exec.replace('{_NodeName_}', node_name) + cmd_to_exec = cmd_to_exec.replace('{_ImageName_}', image_filename) + cmd_to_exec = cmd_to_exec.replace('{_TargetDev_}', 'hda') + cmd_to_exec = cmd_to_exec.replace('{_DevType_}', 'cdrom') + LOG.debug("Ironic node-name: %s, virsh domain name: %s, image_filename: %s" %(task.node.name, node_name, image_filename)) + + _ssh_execute(ssh_obj, cmd_to_exec) + + def _detach_virtual_cd(self, task): + driver_info = _parse_driver_info(task.node) + ssh_obj = _get_ssh_connection(driver_info) + node_name = task.node.name + + if not _get_disk_attachment_status(driver_info, node_name, ssh_obj, 'hda'): + LOG.debug("No CD is attached to node %s, not taking any action", node_name) + return + + cmd_to_exec = "%s %s" % (driver_info['cmd_set']['base_cmd'], + driver_info['cmd_set']['detach_disk_device']) + cmd_to_exec = cmd_to_exec.replace('{_NodeName_}', node_name) + cmd_to_exec = cmd_to_exec.replace('{_TargetDev_}', 'hda') + _ssh_execute(ssh_obj, cmd_to_exec)