Huang Rui 44f02e7848 Use https with CA conf
CA file is a new added config introduced to be used by
Ceilometer-zvm. When zvm inspector needs to communicate with xcat,
it will add the CA file into https communication so it can verify
the xcat is the one that zvm inspector wants to communicate with.

See following link for more info:
https://bugs.launchpad.net/ossn/+bug/1188189

Change-Id: Ib7f1ce2d621f6dd2b5d3b9a63a004abffbc3f223
2016-11-15 11:31:53 +08:00

510 lines
16 KiB
Python

# Copyright 2015 IBM Corp.
#
# 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 functools
import os
from six.moves import http_client as httplib
import socket
import ssl
from ceilometer.compute.virt import inspector
from ceilometer.i18n import _
from ceilometer.i18n import _LW
from oslo_config import cfg
from oslo_log import log as logging
from oslo_serialization import jsonutils
CONF = cfg.CONF
LOG = logging.getLogger(__name__)
class ZVMException(inspector.InspectorException):
pass
class CacheData(object):
"""Virtual machine stat cache."""
_CTYPES = ('cpumem', 'vnics')
def __init__(self):
self._reset()
def _reset(self):
self.cache = dict((tp, {}) for tp in self._CTYPES)
def set(self, ctype, inst_stat):
"""Set or update cache content.
@ctype: cache type.
@inst_stat: cache data.
"""
self.cache[ctype][inst_stat['nodename']] = inst_stat
def get(self, ctype, inst_name):
return self.cache[ctype].get(inst_name, None)
def delete(self, ctype, inst_name):
if inst_name in self.cache[ctype]:
del self.cache[ctype][inst_name]
def clear(self, ctype='all'):
if ctype == 'all':
self._reset()
else:
self.cache[ctype] = {}
class XCATUrl(object):
"""To return xCAT url for invoking xCAT REST API."""
def __init__(self):
self.PREFIX = '/xcatws'
self.SUFFIX = ''.join(('?userName=', CONF.zvm.zvm_xcat_username,
'&password=', CONF.zvm.zvm_xcat_password,
'&format=json'))
# xcat objects
self.NODES = '/nodes'
self.TABLES = '/tables'
# xcat actions
self.XDSH = '/dsh'
def _append_addp(self, rurl, addp=None):
if addp is not None:
return ''.join((rurl, addp))
else:
return rurl
def xdsh(self, arg=''):
"""Run shell command."""
return ''.join((self.PREFIX, self.NODES, arg, self.XDSH, self.SUFFIX))
def gettab(self, arg='', addp=None):
rurl = ''.join((self.PREFIX, self.TABLES, arg, self.SUFFIX))
return self._append_addp(rurl, addp)
def tabdump(self, arg='', addp=None):
return self.gettab(arg, addp)
def lsdef_node(self, arg='', addp=None):
rurl = ''.join((self.PREFIX, self.NODES, arg, self.SUFFIX))
return self._append_addp(rurl, addp)
class HTTPSClientAuthConnection(httplib.HTTPSConnection):
"""For https://wiki.openstack.org/wiki/OSSN/OSSN-0033."""
def __init__(self, host, port, ca_file, timeout=None, key_file=None,
cert_file=None):
httplib.HTTPSConnection.__init__(self, host, port,
key_file=key_file,
cert_file=cert_file)
self.key_file = key_file
self.cert_file = cert_file
self.ca_file = ca_file
self.timeout = timeout
self.use_ca = True
if self.ca_file is None:
LOG.debug("no xCAT CA file specified, this is considered "
"not secure")
self.use_ca = False
def connect(self):
sock = socket.create_connection((self.host, self.port), self.timeout)
if self._tunnel_host:
self.sock = sock
self._tunnel()
if (self.ca_file is not None and
not os.path.exists(self.ca_file)):
LOG.warning(_LW("the CA file %(ca_file) does not exist!"),
{'ca_file': self.ca_file})
self.use_ca = False
if not self.use_ca:
self.sock = ssl.wrap_socket(sock, self.key_file, self.cert_file,
cert_reqs=ssl.CERT_NONE)
else:
self.sock = ssl.wrap_socket(sock, self.key_file, self.cert_file,
ca_certs=self.ca_file,
cert_reqs=ssl.CERT_REQUIRED)
class XCATConnection(object):
"""Https requests to xCAT web service."""
def __init__(self):
"""Initialize https connection to xCAT service."""
self.host = CONF.zvm.zvm_xcat_server
self.port = 443
self.conn = HTTPSClientAuthConnection(self.host, self.port,
CONF.zvm.zvm_xcat_ca_file,
timeout=CONF.zvm.zvm_xcat_connection_timeout)
def request(self, method, url, body=None, headers={}):
"""Send https request to xCAT server.
Will return a python dictionary including:
{'status': http return code,
'reason': http reason,
'message': response message}
"""
if body is not None:
body = jsonutils.dumps(body)
headers = {'content-type': 'text/plain',
'content-length': len(body)}
_rep_ptn = ''.join(('&password=', CONF.zvm.zvm_xcat_password))
LOG.debug("Sending request to xCAT. xCAT-Server:%(xcat_server)s "
"Request-method:%(method)s "
"URL:%(url)s "
"Headers:%(headers)s "
"Body:%(body)s" %
{'xcat_server': CONF.zvm.zvm_xcat_server,
'method': method,
'url': url.replace(_rep_ptn, ''), # hide password in log
'headers': str(headers),
'body': body})
try:
self.conn.request(method, url, body, headers)
except socket.gaierror as err:
msg = (_("Failed to connect xCAT server %(srv)s: %(err)s") %
{'srv': self.host, 'err': err})
raise ZVMException(msg)
except (socket.error, socket.timeout) as err:
msg = (_("Communicate with xCAT server %(srv)s error: %(err)s") %
{'srv': self.host, 'err': err})
raise ZVMException(msg)
try:
res = self.conn.getresponse()
except Exception as err:
msg = (_("Failed to get response from xCAT server %(srv)s: "
"%(err)s") % {'srv': self.host, 'err': err})
raise ZVMException(msg)
msg = res.read()
resp = {
'status': res.status,
'reason': res.reason,
'message': msg}
LOG.debug("xCAT response: %s" % str(resp))
# Only "200" or "201" returned from xCAT can be considered
# as good status
err = None
if method == "POST":
if res.status != 201:
err = str(resp)
else:
if res.status != 200:
err = str(resp)
if err is not None:
msg = (_('Request to xCAT server %(srv)s failed: %(err)s') %
{'srv': self.host, 'err': err})
raise ZVMException(msg)
return resp
def xcat_request(method, url, body=None, headers={}):
conn = XCATConnection()
resp = conn.request(method, url, body, headers)
return load_xcat_resp(resp['message'])
def jsonloads(jsonstr):
try:
return jsonutils.loads(jsonstr)
except ValueError:
errmsg = _("xCAT response data is not in JSON format")
LOG.error(errmsg)
raise ZVMException(errmsg)
@contextlib.contextmanager
def expect_invalid_xcat_resp_data():
"""Catch exceptions when using xCAT response data."""
try:
yield
except (ValueError, TypeError, IndexError, AttributeError,
KeyError) as err:
msg = _("Invalid xCAT response data: %s") % str(err)
raise ZVMException(msg)
def wrap_invalid_xcat_resp_data_error(function):
"""Catch exceptions when using xCAT response data."""
@functools.wraps(function)
def decorated_function(*arg, **kwargs):
try:
return function(*arg, **kwargs)
except (ValueError, TypeError, IndexError, AttributeError,
KeyError) as err:
msg = _("Invalid xCAT response data: %s") % str(err)
raise ZVMException(msg)
return decorated_function
@wrap_invalid_xcat_resp_data_error
def translate_xcat_resp(rawdata, dirt):
"""Translate xCAT response JSON stream to a python dictionary."""
data_list = rawdata.split("\n")
data = {}
for ls in data_list:
for k in dirt.keys():
if ls.__contains__(dirt[k]):
data[k] = ls[(ls.find(dirt[k]) + len(dirt[k])):].strip(' "')
break
return data
@wrap_invalid_xcat_resp_data_error
def load_xcat_resp(message):
"""Abstract information from xCAT REST response body."""
resp_list = jsonloads(message)['data']
keys = ('info', 'data', 'node', 'errorcode', 'error')
resp = {}
for k in keys:
resp[k] = []
for d in resp_list:
for k in keys:
if d.get(k) is not None:
resp[k].append(d.get(k))
err = resp.get('error')
if err != []:
for e in err:
if _is_warning(str(e)):
# ignore known warnings or errors:
continue
else:
raise ZVMException(message)
_log_warnings(resp)
return resp
def _log_warnings(resp):
for msg in (resp['info'], resp['node'], resp['data']):
msgstr = str(msg)
if 'warn' in msgstr.lower():
LOG.warning(_("Warning from xCAT: %s") % msgstr)
def _is_warning(err_str):
ignore_list = (
'Warning: the RSA host key for',
'Warning: Permanently added',
'WARNING: REMOTE HOST IDENTIFICATION HAS CHANGED',
)
for im in ignore_list:
if im in err_str:
return True
return False
def get_userid(node_name):
"""Returns z/VM userid for the xCAT node."""
url = XCATUrl().lsdef_node(''.join(['/', node_name]))
info = xcat_request('GET', url)['info']
with expect_invalid_xcat_resp_data():
for s in info[0]:
if s.__contains__('userid='):
return s.strip().rpartition('=')[2]
def xdsh(node, commands):
""""Run command on xCAT node."""
LOG.debug('Run command %(cmd)s on xCAT node %(node)s' %
{'cmd': commands, 'node': node})
def xdsh_execute(node, commands):
"""Invoke xCAT REST API to execute command on node."""
xdsh_commands = 'command=%s' % commands
# Add -q (quiet) option to ignore ssh warnings and banner msg
opt = 'options=-q'
body = [xdsh_commands, opt]
url = XCATUrl().xdsh('/' + node)
return xcat_request("PUT", url, body)
res_dict = xdsh_execute(node, commands)
return res_dict
def get_node_hostname(node_name):
addp = '&col=node&value=%s&attribute=hostnames' % node_name
url = XCATUrl().gettab("/hosts", addp)
with expect_invalid_xcat_resp_data():
return xcat_request("GET", url)['data'][0][0]
def list_instances(hcp_info):
zvm_host = CONF.zvm.zvm_host
url = XCATUrl().tabdump("/zvm")
res_dict = xcat_request("GET", url)
instances = {}
with expect_invalid_xcat_resp_data():
data_entries = res_dict['data'][0][1:]
for data in data_entries:
l = data.split(",")
node = l[0].strip("\"")
hcp = l[1].strip("\"")
userid = l[2].strip("\"")
# zvm host and zhcp are not included in the list
if (hcp.upper() == hcp_info['hostname'].upper() and
node.upper() not in (zvm_host.upper(),
hcp_info['nodename'].upper(),
CONF.zvm.zvm_xcat_master.upper())):
instances[node] = userid.upper()
return instances
def image_performance_query(zhcp_node, inst_list):
cmd = ('smcli Image_Performance_Query -T "%(inst_list)s" -c %(num)s' %
{'inst_list': " ".join(inst_list), 'num': len(inst_list)})
with expect_invalid_xcat_resp_data():
resp = xdsh(zhcp_node, cmd)
raw_data = resp["data"][0][0]
ipq_kws = {
'userid': "Guest name:",
'guest_cpus': "Guest CPUs:",
'used_cpu_time': "Used CPU time:",
'used_memory': "Used memory:",
}
pi_dict = {}
with expect_invalid_xcat_resp_data():
rpi_list = raw_data.split("".join((zhcp_node, ": \n")))
for rpi in rpi_list:
pi = translate_xcat_resp(rpi, ipq_kws)
if pi.get('userid') is not None:
pi_dict[pi['userid']] = pi
return pi_dict
def get_inst_name(instance):
return getattr(instance, 'OS-EXT-SRV-ATTR:instance_name', None)
def get_inst_power_state(instance):
return getattr(instance, 'OS-EXT-STS:power_state', None)
def virutal_network_vswitch_query_iuo_stats(zhcp_node):
cmd = ('smcli Virtual_Network_Vswitch_Query_IUO_Stats -T "%s" '
'-k "switch_name=*"' % zhcp_node)
with expect_invalid_xcat_resp_data():
resp = xdsh(zhcp_node, cmd)
raw_data_list = resp["data"][0]
while raw_data_list.__contains__(None):
raw_data_list.remove(None)
raw_data = '\n'.join(raw_data_list)
rd_list = raw_data.split('\n')
def _parse_value(data_list, idx, keyword, offset):
return idx + offset, data_list[idx].rpartition(keyword)[2].strip()
vsw_dict = {}
with expect_invalid_xcat_resp_data():
# vswitch count
idx = 0
idx, vsw_count = _parse_value(rd_list, idx, 'vswitch count:', 2)
vsw_dict['vswitch_count'] = int(vsw_count)
# deal with each vswitch data
vsw_dict['vswitches'] = []
for i in range(vsw_dict['vswitch_count']):
vsw_data = {}
# skip vswitch number
idx += 1
# vswitch name
idx, vsw_name = _parse_value(rd_list, idx, 'vswitch name:', 1)
vsw_data['vswitch_name'] = vsw_name
# uplink count
idx, up_count = _parse_value(rd_list, idx, 'uplink count:', 1)
# skip uplink data
idx += int(up_count) * 9
# skip bridge data
idx += 8
# nic count
vsw_data['nics'] = []
idx, nic_count = _parse_value(rd_list, idx, 'nic count:', 1)
nic_count = int(nic_count)
for j in range(nic_count):
nic_data = {}
idx, nic_id = _parse_value(rd_list, idx, 'nic_id:', 1)
userid, toss, vdev = nic_id.partition(' ')
nic_data['userid'] = userid
nic_data['vdev'] = vdev
idx, nic_data['nic_fr_rx'] = _parse_value(rd_list, idx,
'nic_fr_rx:', 1)
idx, nic_data['nic_fr_rx_dsc'] = _parse_value(rd_list, idx,
'nic_fr_rx_dsc:', 1)
idx, nic_data['nic_fr_rx_err'] = _parse_value(rd_list, idx,
'nic_fr_rx_err:', 1)
idx, nic_data['nic_fr_tx'] = _parse_value(rd_list, idx,
'nic_fr_tx:', 1)
idx, nic_data['nic_fr_tx_dsc'] = _parse_value(rd_list, idx,
'nic_fr_tx_dsc:', 1)
idx, nic_data['nic_fr_tx_err'] = _parse_value(rd_list, idx,
'nic_fr_tx_err:', 1)
idx, nic_data['nic_rx'] = _parse_value(rd_list, idx,
'nic_rx:', 1)
idx, nic_data['nic_tx'] = _parse_value(rd_list, idx,
'nic_tx:', 1)
vsw_data['nics'].append(nic_data)
# vlan count
idx, vlan_count = _parse_value(rd_list, idx, 'vlan count:', 1)
# skip vlan data
idx += int(vlan_count) * 3
# skip the blank line
idx += 1
vsw_dict['vswitches'].append(vsw_data)
return vsw_dict