diff --git a/ceilometer_zvm/compute/virt/zvm/inspector.py b/ceilometer_zvm/compute/virt/zvm/inspector.py index 12ed66d..6199488 100644 --- a/ceilometer_zvm/compute/virt/zvm/inspector.py +++ b/ceilometer_zvm/compute/virt/zvm/inspector.py @@ -18,7 +18,26 @@ from oslo_config import cfg from oslo_log import log +zvm_ops = [ + cfg.StrOpt('zvm_xcat_server', + default=None, + help='Host name or IP address of xCAT management_node'), + cfg.StrOpt('zvm_xcat_username', + default=None, + help='xCAT username'), + cfg.StrOpt('zvm_xcat_password', + default=None, + secret=True, + help='Password of the xCAT user'), + cfg.IntOpt('zvm_xcat_connection_timeout', + default=600, + help="The number of seconds wait for xCAT MN response"), +] + + CONF = cfg.CONF +CONF.register_opts(zvm_ops, group='zvm') + LOG = log.getLogger(__name__) diff --git a/ceilometer_zvm/compute/virt/zvm/utils.py b/ceilometer_zvm/compute/virt/zvm/utils.py new file mode 100644 index 0000000..781eb93 --- /dev/null +++ b/ceilometer_zvm/compute/virt/zvm/utils.py @@ -0,0 +1,290 @@ +# 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 httplib +import socket + +from ceilometer.compute.virt import inspector +from ceilometer.i18n import _ +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 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 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.conn = httplib.HTTPSConnection(self.host, + 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 + + if data == {}: + msg = _("No value matched with keywords. Raw Data: %(raw)s; " + "Keywords: %(kws)s") % {'raw': rawdata, 'kws': str(dirt)} + raise ZVMException(msg) + + 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.warn(_("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 + body = [xdsh_commands] + url = XCATUrl().xdsh('/' + node) + return xcat_request("PUT", url, body) + + res_dict = xdsh_execute(node, commands) + + return res_dict diff --git a/ceilometer_zvm/tests/unit/compute/virt/zvm/test_utils.py b/ceilometer_zvm/tests/unit/compute/virt/zvm/test_utils.py new file mode 100644 index 0000000..439d42f --- /dev/null +++ b/ceilometer_zvm/tests/unit/compute/virt/zvm/test_utils.py @@ -0,0 +1,118 @@ +# 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 mock + +from oslo_config import fixture as fixture_config +from oslo_serialization import jsonutils +from oslotest import base + +from ceilometer_zvm.compute.virt.zvm import inspector +from ceilometer_zvm.compute.virt.zvm import utils as zvmutils + + +class TestXCATUrl(base.BaseTestCase): + + def setUp(self): + self.CONF = self.useFixture(fixture_config.Config(inspector.CONF)).conf + self.CONF.set_override('zvm_xcat_username', 'user', 'zvm') + self.CONF.set_override('zvm_xcat_password', 'pwd', 'zvm') + super(TestXCATUrl, self).setUp() + self.xcaturl = zvmutils.XCATUrl() + + def test_xdsh(self): + url = ("/xcatws/nodes/fakenode/dsh" + "?userName=user&password=pwd&format=json") + self.assertEqual(self.xcaturl.xdsh("/fakenode"), url) + + def test_gettab(self): + url = ("/xcatws/tables/table" + "?userName=user&password=pwd&format=json&addp") + self.assertEqual(self.xcaturl.gettab('/table', '&addp'), url) + + def test_lsdef_node(self): + url = ("/xcatws/nodes/fakenode" + "?userName=user&password=pwd&format=json") + self.assertEqual(self.xcaturl.lsdef_node("/fakenode"), url) + + def test_tabdump(self): + url = ("/xcatws/tables/table" + "?userName=user&password=pwd&format=json&addp") + self.assertEqual(self.xcaturl.tabdump('/table', '&addp'), url) + + +class TestXCATConnection(base.BaseTestCase): + + def setUp(self): + self.CONF = self.useFixture(fixture_config.Config(inspector.CONF)).conf + self.CONF.set_override('zvm_xcat_server', '1.1.1.1', 'zvm') + self.CONF.set_override('zvm_xcat_username', 'user', 'zvm') + self.CONF.set_override('zvm_xcat_password', 'pwd', 'zvm') + super(TestXCATConnection, self).setUp() + self.conn = zvmutils.XCATConnection() + + def test_request(self): + with mock.patch.object(self.conn, 'conn') as fake_conn: + fake_res = mock.Mock() + fake_res.status = 200 + fake_res.reason = 'OK' + fake_res.read.return_value = 'data' + fake_conn.getresponse.return_value = fake_res + + exp_data = {'status': 200, + 'reason': 'OK', + 'message': 'data'} + res_data = self.conn.request("GET", 'url') + self.assertEqual(exp_data, res_data) + + def test_request_failed(self): + with mock.patch.object(self.conn, 'conn') as fake_conn: + fake_res = mock.Mock() + fake_res.status = 500 + fake_res.reason = 'INVALID' + fake_res.read.return_value = 'err data' + fake_conn.getresponse.return_value = fake_res + + self.assertRaises(zvmutils.ZVMException, + self.conn.request, 'GET', 'url') + + +class TestZVMUtils(base.BaseTestCase): + + def setUp(self): + self.CONF = self.useFixture(fixture_config.Config(inspector.CONF)).conf + self.CONF.set_override('zvm_xcat_server', '1.1.1.1', 'zvm') + self.CONF.set_override('zvm_xcat_username', 'user', 'zvm') + self.CONF.set_override('zvm_xcat_password', 'pwd', 'zvm') + super(TestZVMUtils, self).setUp() + + @mock.patch('ceilometer_zvm.compute.virt.zvm.utils.XCATConnection.request') + def test_xcat_request(self, xcat_req): + xcat_req.return_value = {'message': jsonutils.dumps( + {'data': [{'data': ['data']}]})} + self.assertEqual([['data']], + zvmutils.xcat_request("GET", 'url')['data']) + + @mock.patch('ceilometer_zvm.compute.virt.zvm.utils.xcat_request') + def test_get_userid(self, xcat_req): + xcat_req.return_value = {'info': [['userid=fakeuser']]} + self.assertEqual('fakeuser', zvmutils.get_userid('fakenode')) + + @mock.patch('ceilometer_zvm.compute.virt.zvm.utils.xcat_request') + def test_xdsh(self, xcat_req): + zvmutils.xdsh('node', 'cmds') + xcat_req.assert_any_call('PUT', + '/xcatws/nodes/node/dsh?userName=user&password=pwd&format=json', + ['command=cmds']) diff --git a/tox.ini b/tox.ini index 652869e..7fd7c50 100644 --- a/tox.ini +++ b/tox.ini @@ -28,7 +28,7 @@ commands = {posargs} commands = python setup.py testr --coverage --testr-args='{posargs}' [flake8] -#ignore = E121,E122,E123,E124,E125,E126,E127,E128,E129,E131,E251,H405 +ignore = E126,E128 exclude = .venv,.git,.tox,dist,doc,*egg,build [hacking]