diff --git a/lower-constraints.txt b/lower-constraints.txt index 4ca91c9545d9..da681ff1968a 100644 --- a/lower-constraints.txt +++ b/lower-constraints.txt @@ -170,3 +170,4 @@ WebOb==1.8.2 websockify==0.8.0 wrapt==1.10.11 wsgi-intercept==1.7.0 +zVMCloudConnector==1.1.1 diff --git a/nova/conf/__init__.py b/nova/conf/__init__.py index 7649e3d66984..0f2f31c0c8f2 100644 --- a/nova/conf/__init__.py +++ b/nova/conf/__init__.py @@ -70,6 +70,7 @@ from nova.conf import workarounds from nova.conf import wsgi from nova.conf import xenserver from nova.conf import xvp +from nova.conf import zvm CONF = cfg.CONF @@ -123,5 +124,6 @@ workarounds.register_opts(CONF) wsgi.register_opts(CONF) xenserver.register_opts(CONF) xvp.register_opts(CONF) +zvm.register_opts(CONF) remote_debug.register_cli_opts(CONF) diff --git a/nova/conf/compute.py b/nova/conf/compute.py index bb2f9583337a..064bf1e230a3 100644 --- a/nova/conf/compute.py +++ b/nova/conf/compute.py @@ -43,6 +43,7 @@ Possible values: * ``vmwareapi.VMwareVCDriver`` * ``hyperv.HyperVDriver`` * ``powervm.PowerVMDriver`` +* ``zvm.ZVMDriver`` """), cfg.BoolOpt('allow_resize_to_same_host', default=False, diff --git a/nova/conf/zvm.py b/nova/conf/zvm.py new file mode 100644 index 000000000000..30b36d1f55ef --- /dev/null +++ b/nova/conf/zvm.py @@ -0,0 +1,51 @@ +# Copyright 2017,2018 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. + +from oslo_config import cfg + + +zvm_opt_group = cfg.OptGroup('zvm', + title='zVM Options', + help=""" +zvm options allows cloud administrator to configure related +z/VM hypervisor driver to be used within an OpenStack deployment. + +zVM options are used when the compute_driver is set to use +zVM (compute_driver=zvm.ZVMDriver) +""") + + +zvm_opts = [ + cfg.URIOpt('cloud_connector_url', + sample_default='http://zvm.example.org:8080/', + help=""" +URL to be used to communicate with z/VM Cloud Connector. +"""), + cfg.StrOpt('ca_file', + default=None, + help=""" +CA certificate file to be verified in httpd server with TLS enabled + +A string, it must be a path to a CA bundle to use. +"""), +] + + +def register_opts(conf): + conf.register_group(zvm_opt_group) + conf.register_opts(zvm_opts, group=zvm_opt_group) + + +def list_opts(): + return {zvm_opt_group: zvm_opts} diff --git a/nova/exception.py b/nova/exception.py index 2d20ff312f32..5fb0b71db958 100644 --- a/nova/exception.py +++ b/nova/exception.py @@ -2312,3 +2312,24 @@ class InstanceUnRescueFailure(NovaException): class IronicAPIVersionNotAvailable(NovaException): msg_fmt = _('Ironic API version %(version)s is not available.') + + +class ZVMDriverException(NovaException): + msg_fmt = _("ZVM Driver has error: %(error)s") + + +class ZVMConnectorError(ZVMDriverException): + msg_fmt = _("zVM Cloud Connector request failed: %(results)s") + + def __init__(self, message=None, **kwargs): + """Exception for zVM ConnectorClient calls. + + :param results: The object returned from ZVMConnector.send_request. + """ + super(ZVMConnectorError, self).__init__(message=message, **kwargs) + + results = kwargs.get('results', {}) + self.overallRC = results.get('overallRC') + self.rc = results.get('rc') + self.rs = results.get('rs') + self.errmsg = results.get('errmsg') diff --git a/nova/tests/unit/virt/zvm/__init__.py b/nova/tests/unit/virt/zvm/__init__.py new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/nova/tests/unit/virt/zvm/test_driver.py b/nova/tests/unit/virt/zvm/test_driver.py new file mode 100644 index 000000000000..2a2eadeafd46 --- /dev/null +++ b/nova/tests/unit/virt/zvm/test_driver.py @@ -0,0 +1,45 @@ +# Copyright 2017,2018 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 nova import exception +from nova import test +from nova.virt.zvm import driver as zvmdriver + + +class TestZVMDriver(test.NoDBTestCase): + + def setUp(self): + super(TestZVMDriver, self).setUp() + self.flags(cloud_connector_url='https://1.1.1.1:1111', group='zvm') + with mock.patch('nova.virt.zvm.utils.' + 'ConnectorClient.call') as mcall: + mcall.return_value = {'hypervisor_hostname': 'TESTHOST'} + self._driver = zvmdriver.ZVMDriver('virtapi') + + def test_driver_init_no_url(self): + self.flags(cloud_connector_url=None, group='zvm') + self.assertRaises(exception.ZVMDriverException, + zvmdriver.ZVMDriver, 'virtapi') + + @mock.patch('nova.virt.zvm.utils.ConnectorClient.call') + def test_get_available_resource_err_case(self, call): + res = {'overallRC': 1, 'errmsg': 'err', 'rc': 0, 'rs': 0} + call.side_effect = exception.ZVMConnectorError(res) + results = self._driver.get_available_resource() + self.assertEqual(0, results['vcpus']) + self.assertEqual(0, results['memory_mb_used']) + self.assertEqual(0, results['disk_available_least']) + self.assertEqual('TESTHOST', results['hypervisor_hostname']) diff --git a/nova/tests/unit/virt/zvm/test_hypervisor.py b/nova/tests/unit/virt/zvm/test_hypervisor.py new file mode 100644 index 000000000000..debe67a8d908 --- /dev/null +++ b/nova/tests/unit/virt/zvm/test_hypervisor.py @@ -0,0 +1,69 @@ +# Copyright 2017,2018 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 nova import exception +from nova import test +from nova.virt.zvm import driver as zvmdriver + + +class TestZVMHypervisor(test.NoDBTestCase): + + def setUp(self): + super(TestZVMHypervisor, self).setUp() + self.flags(cloud_connector_url='https://1.1.1.1:1111', group='zvm') + with mock.patch('nova.virt.zvm.utils.' + 'ConnectorClient.call') as mcall: + mcall.return_value = {'hypervisor_hostname': 'TESTHOST'} + driver = zvmdriver.ZVMDriver('virtapi') + self._hypervisor = driver._hypervisor + + @mock.patch('nova.virt.zvm.utils.ConnectorClient.call') + def test_get_available_resource(self, call): + host_info = {'disk_available': 1144, + 'ipl_time': 'IPL at 11/14/17 10:47:44 EST', + 'vcpus_used': 4, + 'hypervisor_type': 'zvm', + 'disk_total': 2000, + 'zvm_host': 'TESTHOST', + 'memory_mb': 78192, + 'cpu_info': {'cec_model': '2827', + 'architecture': 's390x'}, + 'vcpus': 84, + 'hypervisor_hostname': 'TESTHOST', + 'hypervisor_version': 640, + 'disk_used': 856, + 'memory_mb_used': 8192} + call.return_value = host_info + results = self._hypervisor.get_available_resource() + self.assertEqual(host_info, results) + + @mock.patch('nova.virt.zvm.utils.ConnectorClient.call') + def test_get_available_resource_err_case(self, call): + res = {'overallRC': 1, 'errmsg': 'err', 'rc': 0, 'rs': 0} + call.side_effect = exception.ZVMConnectorError(res) + results = self._hypervisor.get_available_resource() + # Should return an empty dict + self.assertFalse(results) + + def test_get_available_nodes(self): + nodes = self._hypervisor.get_available_nodes() + self.assertEqual(['TESTHOST'], nodes) + + @mock.patch('nova.virt.zvm.utils.ConnectorClient.call') + def test_list_names(self, call): + call.return_value = ['vm1', 'vm2'] + inst_list = self._hypervisor.list_names() + self.assertEqual(['vm1', 'vm2'], inst_list) diff --git a/nova/tests/unit/virt/zvm/test_utils.py b/nova/tests/unit/virt/zvm/test_utils.py new file mode 100644 index 000000000000..5b83044e1c59 --- /dev/null +++ b/nova/tests/unit/virt/zvm/test_utils.py @@ -0,0 +1,81 @@ +# Copyright 2017,2018 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 zvmconnector import connector + +from nova import exception +from nova import test +from nova.virt.zvm import utils as zvmutils + + +class TestZVMUtils(test.NoDBTestCase): + + def setUp(self): + super(TestZVMUtils, self).setUp() + self.flags(cloud_connector_url='http://127.0.0.1', group='zvm') + self._url = 'http://127.0.0.1' + + def test_connector_request_handler_invalid_url(self): + rh = zvmutils.ConnectorClient('http://invalid') + self.assertRaises(exception.ZVMDriverException, rh.call, 'guest_list') + + @mock.patch('zvmconnector.connector.ZVMConnector.__init__', + return_value=None) + def test_connector_request_handler_https(self, mock_init): + rh = zvmutils.ConnectorClient('https://127.0.0.1:80', + ca_file='/tmp/file') + mock_init.assert_called_once_with('127.0.0.1', 80, ssl_enabled=True, + verify='/tmp/file') + self.assertIsInstance(rh._conn, connector.ZVMConnector) + + @mock.patch('zvmconnector.connector.ZVMConnector.__init__', + return_value=None) + def test_connector_request_handler_https_noca(self, mock_init): + rh = zvmutils.ConnectorClient('https://127.0.0.1:80') + mock_init.assert_called_once_with('127.0.0.1', 80, ssl_enabled=True, + verify=False) + self.assertIsInstance(rh._conn, connector.ZVMConnector) + + @mock.patch('zvmconnector.connector.ZVMConnector.__init__', + return_value=None) + def test_connector_request_handler_http(self, mock_init): + rh = zvmutils.ConnectorClient('http://127.0.0.1:80') + mock_init.assert_called_once_with('127.0.0.1', 80, ssl_enabled=False, + verify=False) + self.assertIsInstance(rh._conn, connector.ZVMConnector) + + @mock.patch('zvmconnector.connector.ZVMConnector.send_request') + def test_connector_request_handler(self, mock_send): + mock_send.return_value = {'overallRC': 0, 'output': 'data', + 'rc': 0, 'rs': 0} + rh = zvmutils.ConnectorClient(self._url) + res = rh.call('guest_list') + self.assertEqual('data', res) + + @mock.patch('zvmconnector.connector.ZVMConnector.send_request') + def test_connector_request_handler_error(self, mock_send): + expected = {'overallRC': 1, 'errmsg': 'err', 'rc': 0, 'rs': 0} + mock_send.return_value = expected + + rh = zvmutils.ConnectorClient(self._url) + exc = self.assertRaises(exception.ZVMConnectorError, rh.call, + 'guest_list') + self.assertIn('zVM Cloud Connector request failed', + exc.format_message()) + self.assertEqual(expected['overallRC'], exc.overallRC) + self.assertEqual(expected['rc'], exc.rc) + self.assertEqual(expected['rs'], exc.rs) + self.assertEqual(expected['errmsg'], exc.errmsg) diff --git a/nova/virt/zvm/__init__.py b/nova/virt/zvm/__init__.py new file mode 100644 index 000000000000..183da4234278 --- /dev/null +++ b/nova/virt/zvm/__init__.py @@ -0,0 +1,17 @@ +# Copyright 2017,2018 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. + +from nova.virt.zvm import driver + +ZVMDriver = driver.ZVMDriver diff --git a/nova/virt/zvm/driver.py b/nova/virt/zvm/driver.py new file mode 100644 index 000000000000..537e7b10f9dc --- /dev/null +++ b/nova/virt/zvm/driver.py @@ -0,0 +1,82 @@ +# Copyright 2017,2018 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. + +from oslo_log import log as logging +from oslo_serialization import jsonutils + +from nova import conf +from nova import exception +from nova.i18n import _ +from nova.objects import fields as obj_fields +from nova.virt import driver +from nova.virt.zvm import hypervisor + + +LOG = logging.getLogger(__name__) +CONF = conf.CONF + + +class ZVMDriver(driver.ComputeDriver): + """z/VM implementation of ComputeDriver.""" + + def __init__(self, virtapi): + super(ZVMDriver, self).__init__(virtapi) + + if not CONF.zvm.cloud_connector_url: + error = _('Must specify cloud_connector_url in zvm config ' + 'group to use compute_driver=zvm.driver.ZVMDriver') + raise exception.ZVMDriverException(error=error) + + self._hypervisor = hypervisor.Hypervisor( + CONF.zvm.cloud_connector_url, ca_file=CONF.zvm.ca_file) + + LOG.info("The zVM compute driver has been initialized.") + + def init_host(self, host): + pass + + def list_instances(self): + return self._hypervisor.list_names() + + def get_available_resource(self, nodename=None): + host_stats = self._hypervisor.get_available_resource() + + hypervisor_hostname = self._hypervisor.get_available_nodes()[0] + res = { + 'vcpus': host_stats.get('vcpus', 0), + 'memory_mb': host_stats.get('memory_mb', 0), + 'local_gb': host_stats.get('disk_total', 0), + 'vcpus_used': host_stats.get('vcpus_used', 0), + 'memory_mb_used': host_stats.get('memory_mb_used', 0), + 'local_gb_used': host_stats.get('disk_used', 0), + 'hypervisor_type': host_stats.get('hypervisor_type', + obj_fields.HVType.ZVM), + 'hypervisor_version': host_stats.get('hypervisor_version', ''), + 'hypervisor_hostname': host_stats.get('hypervisor_hostname', + hypervisor_hostname), + 'cpu_info': jsonutils.dumps(host_stats.get('cpu_info', {})), + 'disk_available_least': host_stats.get('disk_available', 0), + 'supported_instances': [(obj_fields.Architecture.S390X, + obj_fields.HVType.ZVM, + obj_fields.VMMode.HVM)], + 'numa_topology': None, + } + + LOG.debug("Getting available resource for %(host)s:%(nodename)s", + {'host': CONF.host, 'nodename': nodename}) + + return res + + def get_available_nodes(self, refresh=False): + return self._hypervisor.get_available_nodes(refresh=refresh) diff --git a/nova/virt/zvm/hypervisor.py b/nova/virt/zvm/hypervisor.py new file mode 100644 index 000000000000..0be0964adf36 --- /dev/null +++ b/nova/virt/zvm/hypervisor.py @@ -0,0 +1,57 @@ +# Copyright 2017,2018 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. + +from oslo_log import log as logging + +from nova import exception +from nova.virt.zvm import utils as zvmutils + + +LOG = logging.getLogger(__name__) + + +class Hypervisor(object): + """z/VM implementation of Hypervisor.""" + + def __init__(self, zcc_url, ca_file=None): + super(Hypervisor, self).__init__() + + self._reqh = zvmutils.ConnectorClient(zcc_url, + ca_file=ca_file) + + # Very very unlikely the hostname will be changed, so when create + # hypervisor object, store the information in the cache and after + # that we can use it directly without query again from connectorclient + self._hypervisor_hostname = self._get_host_info().get( + 'hypervisor_hostname') + + def _get_host_info(self): + host_stats = {} + try: + host_stats = self._reqh.call('host_get_info') + except exception.ZVMConnectorError as e: + LOG.warning("Failed to get host stats: %s", e) + return host_stats + + def get_available_resource(self): + return self._get_host_info() + + def get_available_nodes(self, refresh=False): + # It's not expected that the hostname change, no need to take + # 'refresh' into account. + return [self._hypervisor_hostname] + + def list_names(self): + """list names of the servers in the hypervisor""" + return self._reqh.call('guest_list') diff --git a/nova/virt/zvm/utils.py b/nova/virt/zvm/utils.py new file mode 100644 index 000000000000..0ebed6eee64b --- /dev/null +++ b/nova/virt/zvm/utils.py @@ -0,0 +1,61 @@ +# Copyright 2017,2018 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. + +from oslo_log import log as logging +import six +import six.moves.urllib.parse as urlparse +from zvmconnector import connector + +from nova import exception + + +LOG = logging.getLogger(__name__) + + +class ConnectorClient(object): + """Request handler to zVM cloud connector""" + + def __init__(self, zcc_url, ca_file=None): + _url = urlparse.urlparse(zcc_url) + + _ssl_enabled = False + + if _url.scheme == 'https': + _ssl_enabled = True + elif ca_file: + LOG.warning("url is %(url) which is not https " + "but ca_file is configured to %(ca_file)s", + {'url': zcc_url, 'ca_file': ca_file}) + + if _ssl_enabled and ca_file: + self._conn = connector.ZVMConnector(_url.hostname, _url.port, + ssl_enabled=_ssl_enabled, + verify=ca_file) + else: + self._conn = connector.ZVMConnector(_url.hostname, _url.port, + ssl_enabled=_ssl_enabled, + verify=False) + + def call(self, func_name, *args, **kwargs): + results = self._conn.send_request(func_name, *args, **kwargs) + + if results['overallRC'] != 0: + LOG.error("zVM Cloud Connector request %(api)s failed with " + "parameters: %(args)s %(kwargs)s . Results: %(results)s", + {'api': func_name, 'args': six.text_type(args), + 'kwargs': six.text_type(kwargs), + 'results': six.text_type(results)}) + raise exception.ZVMConnectorError(results=results) + + return results['output'] diff --git a/requirements.txt b/requirements.txt index caf58dcbdd87..2e1f9246a8ab 100644 --- a/requirements.txt +++ b/requirements.txt @@ -66,3 +66,4 @@ retrying>=1.3.3,!=1.3.0 # Apache-2.0 os-service-types>=1.2.0 # Apache-2.0 taskflow>=2.16.0 # Apache-2.0 python-dateutil>=2.5.3 # BSD +zVMCloudConnector>=1.1.1;sys_platform!='win32' # Apache 2.0 License