diff --git a/ironic/conf/console.py b/ironic/conf/console.py index bb3d67cca5..c28f630e6c 100644 --- a/ironic/conf/console.py +++ b/ironic/conf/console.py @@ -13,7 +13,7 @@ # 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_config import cfg from ironic.common.i18n import _ @@ -60,6 +60,30 @@ 'proxy service running on the host of ironic ' 'conductor, in the form of :. This option ' 'is used by both Shellinabox and Socat console')), + cfg.IntOpt('socket_gid', + default=os.getgid(), + help=_('The group id of a potentially created socket')), + cfg.StrOpt('socket_permission', + default='0500', + help=_('Permissions of created unix sockets (octal)')), + cfg.StrOpt('terminal_url_scheme', + default='%(scheme)s://%(host)s:%(port)s', + help=_('Url to the proxy')), + cfg.StrOpt('ssh_command_pattern', + default='sshpass -f %(pw_file)s ssh -l %(username)s %(address)s', + help=_('Command pattern to establish a ssh connection')), + cfg.StrOpt('url_auth_digest_secret', + default='', + help=_('Secret to hash the authentication token with')), + cfg.StrOpt('url_auth_digest_algorithm', + default='md5:base64', + help=_('The digest algorithm (any python hash, followed by either :base64, or :hex')), + cfg.IntOpt('url_auth_digest_expiry', + default=900, + help=_('The validity of the token in seconds')), + cfg.StrOpt('url_auth_digest_pattern', + default='%(expiry)s%(uuid)s %(secret)s', + help=_('Python string formatting pattern, with expiry, uuid, and secret')), ] diff --git a/ironic/drivers/modules/console_utils.py b/ironic/drivers/modules/console_utils.py index 6e08b67128..5baf721621 100644 --- a/ironic/drivers/modules/console_utils.py +++ b/ironic/drivers/modules/console_utils.py @@ -20,6 +20,7 @@ """ import errno +import hashlib import fcntl import ipaddress import os @@ -28,6 +29,8 @@ import subprocess import time +from base64 import urlsafe_b64encode +from datetime import datetime, timedelta from ironic_lib import utils as ironic_utils from oslo_concurrency import lockutils from oslo_log import log as logging @@ -73,6 +76,15 @@ def _ensure_console_pid_dir_exists(): raise exception.ConsoleError(message=msg) +def _get_console_unix_socket(node_uuid): + """Generate the unix socket file name.""" + + pid_dir = _get_console_pid_dir() + name = "%s.sock" % node_uuid + path = os.path.join(pid_dir, name) + return path + + def _get_console_pid_file(node_uuid): """Generate the pid file name to hold the terminal process id.""" @@ -215,17 +227,39 @@ def release_port(port): ALLOCATED_PORTS.discard(port) -def get_shellinabox_console_url(port): +def get_shellinabox_console_url(port, uuid=None): """Get a url to access the console via shellinaboxd. :param port: the terminal port for the node. """ + digest = None + expiry = None + if CONF.console.url_auth_digest_secret: + try: + hash_algorithm, digest_algorithm = CONF.console.url_auth_digest_algorithm.split(':', 1) + h = hashlib.new(hash_algorithm) + expiry = int((datetime.utcnow() - datetime(1970,1,1) + timedelta(seconds=CONF.console.url_auth_digest_expiry)).total_seconds()) + to_sign = CONF.console.url_auth_digest_pattern % { + 'uuid': uuid, + 'expiry': expiry, + 'secret': CONF.console.url_auth_digest_secret } + h.update(to_sign) + if digest_algorithm == 'base64': + digest = urlsafe_b64encode(h.digest()) + else: + digest = h.hexdigest() + except ValueError as e: + LOG.warning("Could not setup authenticated url due to %s", e) + console_host = utils.wrap_ipv6(CONF.my_ip) scheme = 'https' if CONF.console.terminal_cert_dir else 'http' - return '%(scheme)s://%(host)s:%(port)s' % {'scheme': scheme, + return CONF.console.terminal_url_scheme % {'scheme': scheme, 'host': console_host, - 'port': port} + 'port': port, + 'uuid': uuid, + 'digest': digest, + 'expiry': expiry } class _PopenNonblockingPipe(object): @@ -292,8 +326,17 @@ def start_shellinabox_console(node_uuid, port, console_cmd): args.append(CONF.console.terminal_cert_dir) else: args.append("-t") - args.append("-p") - args.append(str(port)) + if port == 'unix' or port is None: + args.append('--unixdomain-only') + args.append(('%(path)s:%(uid)s:%(gid)s:%(mode)s' % { + 'path': _get_console_unix_socket(node_uuid), + 'uid': os.getuid(), + 'gid': CONF.console.socket_gid, + 'mode': CONF.console.socket_permission })) + else: + args.append("-p") + args.append(str(port)) + args.append("--background=%s" % pid_file) args.append("-s") args.append(console_cmd) diff --git a/ironic/drivers/modules/ipmitool.py b/ironic/drivers/modules/ipmitool.py index b1c20c968f..9383288eeb 100644 --- a/ironic/drivers/modules/ipmitool.py +++ b/ironic/drivers/modules/ipmitool.py @@ -1482,10 +1482,6 @@ def validate(self, task): """ driver_info = _parse_driver_info(task.node) - if not driver_info['port'] and CONF.console.port_range is None: - raise exception.MissingParameterValue(_( - "Either missing 'ipmi_terminal_port' parameter in node's " - "driver_info or [console]port_range is not configured")) if driver_info['protocol_version'] != '2.0': raise exception.InvalidParameterValue(_( @@ -1581,7 +1577,7 @@ def stop_console(self, task): def get_console(self, task): """Get the type and connection information about the console.""" driver_info = _parse_driver_info(task.node) - url = console_utils.get_shellinabox_console_url(driver_info['port']) + url = console_utils.get_shellinabox_console_url(port=driver_info['port'], uuid=task.node.uuid) return {'type': 'shellinabox', 'url': url}