252 lines
8.0 KiB
Python
252 lines
8.0 KiB
Python
#
|
|
# Copyright 2016 Cray 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 contextlib
|
|
import copy
|
|
import hashlib
|
|
import os
|
|
import subprocess
|
|
import tempfile
|
|
|
|
import six
|
|
from oslo_concurrency import processutils
|
|
from oslo_config import cfg
|
|
from oslo_utils import strutils
|
|
|
|
from ironic.common import dhcp_factory
|
|
from ironic.common import exception
|
|
from ironic.common import keystone
|
|
from ironic.common import utils
|
|
from ironic.common.i18n import _, _LW
|
|
from ironic.openstack.common import log as logging
|
|
|
|
LOG = logging.getLogger(__name__)
|
|
CONF = cfg.CONF
|
|
|
|
|
|
def get_service_tenant_id():
|
|
ksclient = keystone._get_ksclient()
|
|
if not keystone._is_apiv3(CONF.keystone_authtoken.auth_uri,
|
|
CONF.keystone_authtoken.auth_version):
|
|
tenant_name = CONF.keystone_authtoken.admin_tenant_name
|
|
if tenant_name:
|
|
return ksclient.tenants.find(name=tenant_name).to_dict()['id']
|
|
|
|
|
|
def change_node_dict(node, dict_name, new_data):
|
|
"""Workaround for Ironic object model to update dict."""
|
|
dict_data = getattr(node, dict_name).copy()
|
|
dict_data.update(new_data)
|
|
setattr(node, dict_name, dict_data)
|
|
|
|
|
|
def str_to_alnum(s):
|
|
if not s.isalnum():
|
|
s = ''.join([c for c in s if c.isalnum()])
|
|
return s
|
|
|
|
|
|
def str_replace_non_alnum(s, replace_by="_"):
|
|
if not s.isalnum():
|
|
s = ''.join([(c if c.isalnum() else replace_by) for c in s])
|
|
return s
|
|
|
|
|
|
def validate_json(required, raw):
|
|
for k in required:
|
|
if k not in raw:
|
|
raise exception.MissingParameterValue(
|
|
"%s object missing %s parameter"
|
|
% (str(raw), k)
|
|
)
|
|
|
|
|
|
def get_node_ip(task):
|
|
provider = dhcp_factory.DHCPFactory()
|
|
addresses = provider.provider.get_ip_addresses(task)
|
|
if addresses:
|
|
return addresses[0]
|
|
return None
|
|
|
|
|
|
def get_ssh_connection(task, **kwargs):
|
|
ssh = utils.ssh_connect(kwargs)
|
|
|
|
# Note(oberezovskyi): this is required to prevent printing private_key to
|
|
# the conductor log
|
|
if kwargs.get('key_contents'):
|
|
kwargs['key_contents'] = '*****'
|
|
|
|
LOG.debug("SSH with params:")
|
|
LOG.debug(kwargs)
|
|
|
|
return ssh
|
|
|
|
|
|
@contextlib.contextmanager
|
|
def ssh_tunnel(port, user, key_file, target_host, ssh_port=22):
|
|
tunnel = _create_ssh_tunnel(port, port, user, key_file, target_host,
|
|
local_forwarding=False)
|
|
try:
|
|
yield
|
|
finally:
|
|
tunnel.terminate()
|
|
|
|
|
|
def _create_ssh_tunnel(remote_port, local_port, user, key_file, target_host,
|
|
remote_ip='127.0.0.1', local_ip='127.0.0.1',
|
|
local_forwarding=True,
|
|
ssh_port=22):
|
|
cmd = ['ssh', '-N', '-o', 'StrictHostKeyChecking=no', '-o',
|
|
'UserKnownHostsFile=/dev/null', '-p', str(ssh_port), '-i', key_file]
|
|
if local_forwarding:
|
|
cmd += ['-L', '{}:{}:{}:{}'.format(local_ip, local_port, remote_ip,
|
|
remote_port)]
|
|
else:
|
|
cmd += ['-R', '{}:{}:{}:{}'.format(remote_ip, remote_port, local_ip,
|
|
local_port)]
|
|
|
|
cmd.append('@'.join((user, target_host)))
|
|
# TODO(lobur): Make this sync, check status. (may use ssh control socket).
|
|
return subprocess.Popen(cmd)
|
|
|
|
|
|
def sftp_write_to(sftp, data, path):
|
|
with tempfile.NamedTemporaryFile(dir=CONF.tempdir) as f:
|
|
f.write(data)
|
|
f.flush()
|
|
sftp.put(f.name, path)
|
|
|
|
|
|
def sftp_ensure_tree(sftp, path):
|
|
try:
|
|
sftp.mkdir(path)
|
|
except IOError:
|
|
pass
|
|
|
|
|
|
# TODO(oberezovskyi): merge this code with processutils.ssh_execute
|
|
def ssh_execute(ssh, cmd, process_input=None,
|
|
addl_env=None, check_exit_code=True,
|
|
binary=False, timeout=None):
|
|
sanitized_cmd = strutils.mask_password(cmd)
|
|
LOG.debug('Running cmd (SSH): %s', sanitized_cmd)
|
|
if addl_env:
|
|
raise exception.InvalidArgumentError(
|
|
_('Environment not supported over SSH'))
|
|
|
|
if process_input:
|
|
# This is (probably) fixable if we need it...
|
|
raise exception.InvalidArgumentError(
|
|
_('process_input not supported over SSH'))
|
|
|
|
stdin_stream, stdout_stream, stderr_stream = ssh.exec_command(cmd)
|
|
channel = stdout_stream.channel
|
|
|
|
if timeout and not channel.status_event.wait(timeout=timeout):
|
|
raise exception.SSHCommandFailed(cmd=cmd)
|
|
|
|
# NOTE(justinsb): This seems suspicious...
|
|
# ...other SSH clients have buffering issues with this approach
|
|
stdout = stdout_stream.read()
|
|
stderr = stderr_stream.read()
|
|
|
|
stdin_stream.close()
|
|
|
|
exit_status = channel.recv_exit_status()
|
|
|
|
if six.PY3:
|
|
# Decode from the locale using using the surrogateescape error handler
|
|
# (decoding cannot fail). Decode even if binary is True because
|
|
# mask_password() requires Unicode on Python 3
|
|
stdout = os.fsdecode(stdout)
|
|
stderr = os.fsdecode(stderr)
|
|
stdout = strutils.mask_password(stdout)
|
|
stderr = strutils.mask_password(stderr)
|
|
|
|
# exit_status == -1 if no exit code was returned
|
|
if exit_status != -1:
|
|
LOG.debug('Result was %s' % exit_status)
|
|
if check_exit_code and exit_status != 0:
|
|
raise processutils.ProcessExecutionError(exit_code=exit_status,
|
|
stdout=stdout,
|
|
stderr=stderr,
|
|
cmd=sanitized_cmd)
|
|
|
|
if binary:
|
|
if six.PY2:
|
|
# On Python 2, stdout is a bytes string if mask_password() failed
|
|
# to decode it, or an Unicode string otherwise. Encode to the
|
|
# default encoding (ASCII) because mask_password() decodes from
|
|
# the same encoding.
|
|
if isinstance(stdout, unicode):
|
|
stdout = stdout.encode()
|
|
if isinstance(stderr, unicode):
|
|
stderr = stderr.encode()
|
|
else:
|
|
# fsencode() is the reverse operation of fsdecode()
|
|
stdout = os.fsencode(stdout)
|
|
stderr = os.fsencode(stderr)
|
|
|
|
return (stdout, stderr)
|
|
|
|
|
|
def umount_without_raise(loc, *args):
|
|
"""Helper method to umount without raise."""
|
|
try:
|
|
utils.umount(loc, *args)
|
|
except processutils.ProcessExecutionError as e:
|
|
LOG.warn(_LW("umount_without_raise unable to umount dir %(path)s, "
|
|
"error: %(e)s"), {'path': loc, 'e': e})
|
|
|
|
|
|
def md5(url):
|
|
"""Generate md5 has for the sting."""
|
|
return hashlib.md5(url).hexdigest()
|
|
|
|
|
|
class RawToPropertyMixin(object):
|
|
"""A helper mixin for json-based entities.
|
|
|
|
Should be used for the json-based class definitions. If you have a class
|
|
corresponding to a json, use this mixin to get direct json <-> class
|
|
attribute mapping. It also gives out-of-the-box serialization back to json.
|
|
"""
|
|
|
|
_raw = {}
|
|
|
|
def __getattr__(self, item):
|
|
if not self._is_special_name(item):
|
|
return self._raw.get(item)
|
|
|
|
def __setattr__(self, key, value):
|
|
if (not self._is_special_name(key)) and (key not in self.__dict__):
|
|
self._raw[key] = value
|
|
else:
|
|
self.__dict__[key] = value
|
|
|
|
def _is_special_name(self, name):
|
|
return name.startswith("_") or name.startswith("__")
|
|
|
|
def to_dict(self):
|
|
data = {}
|
|
for k, v in self._raw.iteritems():
|
|
if (isinstance(v, list) and len(v) > 0 and
|
|
isinstance(v[0], RawToPropertyMixin)):
|
|
data[k] = [r.to_dict() for r in v]
|
|
else:
|
|
data[k] = v
|
|
return copy.deepcopy(data)
|