diff --git a/.gitignore b/.gitignore new file mode 100755 index 0000000..faadcd5 --- /dev/null +++ b/.gitignore @@ -0,0 +1,9 @@ +*.egg* +*.log +*.pyc +.project +.pydevproject +.tox +.venv +build/* +dist/* diff --git a/.gitreview b/.gitreview old mode 100644 new mode 100755 diff --git a/README.rst b/README.rst new file mode 100644 index 0000000..34695ec --- /dev/null +++ b/README.rst @@ -0,0 +1,5 @@ +This project contains a set of drivers and plugins for different OpenStack +components that enables OpenStack manage z/VM hypervisor and virtual machines +running in the z/VM system. + +Wiki page: https://wiki.openstack.org/wiki/ZVMDriver diff --git a/neutron-zvm-plugin/neutron/plugins/zvm/__init__.py b/neutron-zvm-plugin/neutron/plugins/zvm/__init__.py new file mode 100755 index 0000000..e69de29 diff --git a/neutron-zvm-plugin/neutron/plugins/zvm/agent/__init__.py b/neutron-zvm-plugin/neutron/plugins/zvm/agent/__init__.py new file mode 100755 index 0000000..e69de29 diff --git a/neutron-zvm-plugin/neutron/plugins/zvm/agent/zvm_network.py b/neutron-zvm-plugin/neutron/plugins/zvm/agent/zvm_network.py new file mode 100755 index 0000000..1dd4354 --- /dev/null +++ b/neutron-zvm-plugin/neutron/plugins/zvm/agent/zvm_network.py @@ -0,0 +1,62 @@ +# vim: tabstop=4 shiftwidth=4 softtabstop=4 + +# Copyright 2014 IBM Corp. +# +# 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. + +from oslo.config import cfg +from neutron.openstack.common import log as logging +from neutron.plugins.common import utils as plugin_utils +from neutron.plugins.zvm.common import utils + +LOG = logging.getLogger(__name__) + +vswitch_opts = [ + cfg.StrOpt('rdev_list', + help='RDev list for vswitch uplink port')] + +CONF = cfg.CONF +CONF.import_opt('flat_networks', "neutron.plugins.ml2.drivers.type_flat", + 'ml2_type_flat') +CONF.import_opt('network_vlan_ranges', "neutron.plugins.ml2.drivers.type_vlan", + 'ml2_type_vlan') + + +class zvmVswitch(object): + def __init__(self, zhcp, name, vlan): + self._utils = utils.zvmUtils() + self._utils.add_vswitch(zhcp, name, + eval("CONF." + name + ".rdev_list"), vid=vlan) + self.zhcp = zhcp + + +class zvmNetwork(object): + def __init__(self): + self._zhcp = CONF.AGENT.xcat_zhcp_nodename + self._vsws = [] + self._maps = {} + self._creat_networks() + + def _creat_networks(self): + self._maps = plugin_utils.parse_network_vlan_ranges( + CONF.ml2_type_vlan.network_vlan_ranges + + CONF.ml2_type_flat.flat_networks) + self._vsws = [] + for vsw in self._maps.keys(): + CONF.register_opts(vswitch_opts, vsw) + self._vsws.append(zvmVswitch(self._zhcp, vsw, self._maps[vsw])) + + def get_network_maps(self): + return self._maps diff --git a/neutron-zvm-plugin/neutron/plugins/zvm/agent/zvm_neutron_agent.py b/neutron-zvm-plugin/neutron/plugins/zvm/agent/zvm_neutron_agent.py new file mode 100755 index 0000000..e3f4a09 --- /dev/null +++ b/neutron-zvm-plugin/neutron/plugins/zvm/agent/zvm_neutron_agent.py @@ -0,0 +1,350 @@ +# vim: tabstop=4 shiftwidth=4 softtabstop=4 + +# Copyright 2014 IBM Corp. +# +# 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 eventlet +import sys +import time +import zvm_network + +from oslo.config import cfg + +from neutron import context +from neutron.agent import rpc as agent_rpc +from neutron.common import config as common_config +from neutron.common import constants as q_const +from neutron.common import rpc as n_rpc +from neutron.common import topics +from neutron.plugins.common import constants as p_const +from neutron.openstack.common import log as logging +from neutron.openstack.common import loopingcall +from neutron.openstack.common.gettextutils import _ +from neutron.plugins.zvm.common import exception +from neutron.plugins.zvm.common import utils + +LOG = logging.getLogger(__name__) + + +def restart_wrapper(func): + def wrapper(*args, **kw): + gen = func(*args, **kw) + gen.next() + return gen + return wrapper + + +class zvmNeutronAgent(n_rpc.RpcCallback): + RPC_API_VERSION = '1.1' + + def __init__(self): + super(zvmNeutronAgent, self).__init__() + self._utils = utils.zvmUtils() + self._polling_interval = cfg.CONF.AGENT.polling_interval + self._zhcp_node = cfg.CONF.AGENT.xcat_zhcp_nodename + self._host = cfg.CONF.AGENT.zvm_host + + zvm_net = zvm_network.zvmNetwork() + self.agent_state = { + 'binary': 'neutron-zvm-agent', + 'host': self._host, + 'topic': q_const.L2_AGENT_TOPIC, + 'configurations': {'vswitch_mappings': zvm_net.get_network_maps()}, + 'agent_type': q_const.AGENT_TYPE_ZVM, + 'start_flag': True} + self._setup_server_rpc() + self._zhcp_userid = self._utils.get_zhcp_userid(self._zhcp_node) + self._restart_handler = self._handle_restart() + + def _setup_server_rpc(self): + self.agent_id = 'zvm_agent_%s' % self._zhcp_node + self.topic = topics.AGENT + self.plugin_rpc = agent_rpc.PluginApi(topics.PLUGIN) + self.state_rpc = agent_rpc.PluginReportStateAPI(topics.PLUGIN) + + self.context = context.get_admin_context_without_session() + + self.endpoints = [self] + consumers = [[topics.PORT, topics.UPDATE], + [topics.NETWORK, topics.DELETE]] + self.connection = agent_rpc.create_consumers(self.endpoints, + self.topic, + consumers) + + report_interval = cfg.CONF.AGENT.report_interval + if report_interval: + heartbeat = loopingcall.FixedIntervalLoopingCall( + self._report_state) + heartbeat.start(interval=report_interval) + + def _report_state(self): + try: + self.state_rpc.report_state(self.context, self.agent_state) + self.agent_state.pop('start_flag', None) + except Exception: + LOG.exception(_("Failed reporting state!")) + + def network_delete(self, context, network_id=None): + LOG.debug(_("Network delete received. UUID: %s"), network_id) + + def port_update(self, context, **kwargs): + port = kwargs.get('port') + LOG.debug(_("Port update received. UUID: %s"), port) + + if not port['id'] in self._port_map.keys(): + # update a port which is not coupled to any NIC, nothing + # to do for a user based vswitch + return + + vswitch = self._port_map[port['id']]['vswitch'] + userid = self._port_map[port['id']]['userid'] + if port['admin_state_up']: + self._utils.couple_nic_to_vswitch(vswitch, port['id'], + self._zhcp_node, userid) + self.plugin_rpc.update_device_up(self.context, port['id'], + self.agent_id) + else: + self._utils.uncouple_nic_from_vswitch(vswitch, port['id'], + self._zhcp_node, userid) + self.plugin_rpc.update_device_down(self.context, port['id'], + self.agent_id) + self._utils.put_user_direct_online(self._zhcp_node, + self._zhcp_userid) + + def port_bound(self, port_id, net_uuid, + network_type, physical_network, segmentation_id, userid): + LOG.debug(_("Binding port %s"), port_id) + + self._utils.grant_user(self._zhcp_node, physical_network, userid) + vdev = self._utils.couple_nic_to_vswitch(physical_network, port_id, + self._zhcp_node, userid) + self._utils.put_user_direct_online(self._zhcp_node, + self._zhcp_userid) + + if network_type == p_const.TYPE_VLAN: + LOG.info(_('Binding VLAN, VLAN ID: %s'), segmentation_id) + self._utils.set_vswitch_port_vlan_id(segmentation_id, port_id, + vdev, self._zhcp_node, + physical_network) + else: + LOG.info(_('Bind %s mode done'), network_type) + + def port_unbound(self, port_id): + LOG.debug(_("Unbinding port %s"), port_id) + # uncouple is not necessary, because revoke user will uncouple it + # automatically. + self._utils.revoke_user(self._zhcp_node, + self._port_map[port_id]['vswitch'], + self._port_map[port_id]['userid']) + + def _update_ports(self, registered_ports): + ports_info = self._utils.get_nic_ids() + ports = set() + for p in ports_info: + target_host = p.split(',')[5].strip('"') + new_port_id = p.split(',')[2].strip('"') + if target_host == self._zhcp_node: + ports.add(new_port_id) + + if ports == registered_ports: + return + + added = ports - registered_ports + removed = registered_ports - ports + return {'current': ports, 'added': added, 'removed': removed} + + def _treat_vif_port(self, port_id, network_id, network_type, + physical_network, segmentation_id, + admin_state_up): + node = self._utils.get_node_from_port(port_id) + userid = self._utils.get_userid_from_node(node) + LOG.info(_("Update port for node:%s") % node) + if admin_state_up: + self.port_bound(port_id, network_id, network_type, + physical_network, segmentation_id, + userid) + else: + self._utils.grant_user(self._zhcp_node, physical_network, userid) + return (node, userid) + + def _treat_devices_added(self, devices): + for device in devices: + LOG.info(_("Adding port %s") % device) + try: + details = self.plugin_rpc.get_device_details(self.context, + device, + self.agent_id) + except Exception: + LOG.debug(_("Unable to get port details for %s:"), device) + continue + + try: + if 'port_id' in details: + LOG.info(_("Port %(device)s updated." + " Details: %(details)s"), + {'device': device, 'details': details}) + (node, userid) = self._treat_vif_port( + details['port_id'], + details['network_id'], + details['network_type'], + details['physical_network'], + details['segmentation_id'], + details['admin_state_up']) + # add device done, keep port map info + self._port_map[device] = {} + self._port_map[device]['userid'] = userid + self._port_map[device]['nodename'] = node + self._port_map[device]['vswitch'] = details[ + 'physical_network'] + self._port_map[device]['vlan_id'] = details[ + 'segmentation_id'] + + # no rollback if this fails + self._utils.update_xcat_switch(details['port_id'], + details['physical_network'], + details['segmentation_id']) + if details.get('admin_state_up'): + LOG.debug(_("Setting status for %s to UP"), device) + self.plugin_rpc.update_device_up( + self.context, device, self.agent_id, cfg.CONF.host) + else: + LOG.debug(_("Setting status for %s to DOWN"), device) + self.plugin_rpc.update_device_down( + self.context, device, self.agent_id, cfg.CONF.host) + + else: + LOG.debug(_("Device %s not defined on Neutron server"), + device) + continue + except Exception as e: + LOG.exception(_("Can not add device %(device)s: %(msg)s"), + {'device': device, 'msg': e}) + continue + + def _treat_devices_removed(self, devices): + for device in devices: + LOG.info(_("Removing port %s"), device) + try: + if not device in self._port_map: + LOG.warn(_("Can't find port %s in zvm agent"), device) + continue + + self.port_unbound(device) + self.plugin_rpc.update_device_down(self.context, + device, + self.agent_id) + del self._port_map[device] + except Exception as e: + LOG.exception(_("Removing port failed %(device)s: %(msg)s"), + {'device': device, 'msg': e}) + continue + + def _process_network_ports(self, port_info): + if len(port_info['added']): + self._treat_devices_added(port_info['added']) + if len(port_info['removed']): + self._treat_devices_removed(port_info['removed']) + + def xcatdb_daemon_loop(self): + ports = set() + # Get all exsited ports as configured + all_ports_info = self._update_ports(ports) + if all_ports_info is not None: + ports = all_ports_info['current'] + + connect = True + while True: + try: + start_time = time.time() + port_info = self._update_ports(ports) + + # if no exception is raised in _update_ports, + # then the connection has recovered + if connect is False: + self._restart_handler.send(None) + connect = True + + if port_info: + LOG.debug(_("Devices change!")) + self._process_network_ports(port_info) + ports = port_info['current'] + except exception.zVMxCatRequestFailed as e: + LOG.error(_("Lost connection to xCAT. %s"), e) + connect = False + except Exception as e: + LOG.exception(_("error in xCAT DB query loop: %s"), e) + + # sleep till end of polling interval + elapsed = (time.time() - start_time) + if (elapsed < self._polling_interval): + sleep_time = self._polling_interval - elapsed + LOG.debug(_("Sleep %s"), sleep_time) + time.sleep(sleep_time) + else: + LOG.debug(_("Looping iteration exceeded interval")) + + def _init_xcat_mgt(self): + '''xCAT Management Node(MN) use the first flat network to manage all + the instances. So a flat network is required. + To talk to xCAT MN, xCAT MN requires every instance has a NIC which is + in the same subnet as xCAT. The xCAT MN's IP address is xcat_mgt_ip, + mask is xcat_mgt_mask in the config file, + by default neutron_zvm_plugin.ini. + ''' + + if not len(cfg.CONF.ml2_type_flat.flat_networks): + raise exception.zvmException( + msg=_('Can not find xCAT management network,' + 'a flat network is required by xCAT.')) + self._utils.create_xcat_mgt_network(self._zhcp_node, + cfg.CONF.AGENT.xcat_mgt_ip, + cfg.CONF.AGENT.xcat_mgt_mask, + cfg.CONF.ml2_type_flat.flat_networks[0]) + + @restart_wrapper + def _handle_restart(self): + xcat_uptime, zvm_uptime = (None, None) + while True: + LOG.info(_("Try to reinitialize network ... ")) + try: + tmp_new_time = self._utils.query_xcat_uptime(self._zhcp_node) + if xcat_uptime != tmp_new_time: + self._init_xcat_mgt() + xcat_uptime = tmp_new_time + + tmp_new_time = self._utils.query_zvm_uptime(self._zhcp_node) + if zvm_uptime != tmp_new_time: + self._port_map = self._utils.re_grant_user(self._zhcp_node) + zvm_uptime = tmp_new_time + yield + except Exception: + LOG.error(_("Failed to handle restart," + "try again in 5 seconds")) + time.sleep(5) + + +def main(): + eventlet.monkey_patch() + cfg.CONF(project='neutron') + common_config.init(sys.argv[1:]) + common_config.setup_logging() + + agent = zvmNeutronAgent() + + # Start to query xCAT DB + agent.xcatdb_daemon_loop() + LOG.info(_("z/VM agent initialized, now running... ")) + sys.exit(0) diff --git a/neutron-zvm-plugin/neutron/plugins/zvm/common/__init__.py b/neutron-zvm-plugin/neutron/plugins/zvm/common/__init__.py new file mode 100755 index 0000000..e69de29 diff --git a/neutron-zvm-plugin/neutron/plugins/zvm/common/config.py b/neutron-zvm-plugin/neutron/plugins/zvm/common/config.py new file mode 100755 index 0000000..fd4718b --- /dev/null +++ b/neutron-zvm-plugin/neutron/plugins/zvm/common/config.py @@ -0,0 +1,66 @@ +# vim: tabstop=4 shiftwidth=4 softtabstop=4 + +# Copyright 2014 IBM Corp. +# +# 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. + +from oslo.config import cfg +from neutron.agent.common import config +from neutron.openstack.common.gettextutils import _ + +agent_opts = [ + cfg.StrOpt( + 'xcat_zhcp_nodename', + default='zhcp', + help=_('xCat zHCP nodename in xCAT ')), + cfg.StrOpt( + 'zvm_host', + help=_('z/VM host that managed by zHCP.')), + cfg.StrOpt( + 'zvm_xcat_username', + default='admin', + help=_('xCat REST API username')), + cfg.StrOpt( + 'zvm_xcat_password', + default='admin', + secret=True, + help=_('Password of the xCat REST API user')), + cfg.StrOpt( + 'zvm_xcat_server', + help=_("xCat MN server address")), + cfg.IntOpt( + 'polling_interval', + default=2, + help=_("The number of seconds the agent will wait between " + "polling for local device changes.")), + cfg.IntOpt( + 'zvm_xcat_timeout', + default=300, + help=_("The number of seconds the agent will wait for " + "xCAT MN response")), + cfg.StrOpt( + 'xcat_mgt_ip', + default=None, + help=_("The IP address is used for xCAT MN to management instances.")), + cfg.StrOpt( + 'xcat_mgt_mask', + default=None, + help=_("The IP mask is used for xCAT MN to management instances.")), +] + +CONF = cfg.CONF +CONF.register_opts(agent_opts, "AGENT") +config.register_agent_state_opts_helper(cfg.CONF) +config.register_root_helper(cfg.CONF) diff --git a/neutron-zvm-plugin/neutron/plugins/zvm/common/constants.py b/neutron-zvm-plugin/neutron/plugins/zvm/common/constants.py new file mode 100755 index 0000000..2615557 --- /dev/null +++ b/neutron-zvm-plugin/neutron/plugins/zvm/common/constants.py @@ -0,0 +1,17 @@ +# vim: tabstop=4 shiftwidth=4 softtabstop=4 + +# Copyright 2014 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. + +XCAT_RESPONSE_KEYS = ('info', 'data', 'node', 'errorcode', 'error') diff --git a/neutron-zvm-plugin/neutron/plugins/zvm/common/exception.py b/neutron-zvm-plugin/neutron/plugins/zvm/common/exception.py new file mode 100755 index 0000000..f11a646 --- /dev/null +++ b/neutron-zvm-plugin/neutron/plugins/zvm/common/exception.py @@ -0,0 +1,43 @@ +# vim: tabstop=4 shiftwidth=4 softtabstop=4 + +# Copyright 2014 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 neutron.common import exceptions as exception +from neutron.openstack.common.gettextutils import _ + + +class zvmException(exception.NeutronException): + message = _('zvmException: %(msg)s') + + +class zVMxCatConnectionFailed(exception.NeutronException): + message = _('Failed to connect xCAT server: %(xcatserver)s') + + +class zVMxCatRequestFailed(exception.NeutronException): + message = _('Request to xCAT server %(xcatserver)s failed: %(err)s') + + +class zVMJsonLoadsError(exception.NeutronException): + message = _('JSON loads error: not in JSON format') + + +class zVMInvalidDataError(exception.NeutronException): + message = _('Invalid data error: %(msg)s') + + +class zVMInvalidxCatResponseDataError(exception.NeutronException): + message = _('Invalid data returned from xCAT: %(msg)s') diff --git a/neutron-zvm-plugin/neutron/plugins/zvm/common/utils.py b/neutron-zvm-plugin/neutron/plugins/zvm/common/utils.py new file mode 100755 index 0000000..ed0e4aa --- /dev/null +++ b/neutron-zvm-plugin/neutron/plugins/zvm/common/utils.py @@ -0,0 +1,415 @@ +# vim: tabstop=4 shiftwidth=4 softtabstop=4 + +# Copyright 2014 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 xcatutils + +from oslo.config import cfg +from neutron.openstack.common import log as logging +from neutron.openstack.common.gettextutils import _ +from neutron.plugins.zvm.common import exception + +CONF = cfg.CONF +LOG = logging.getLogger(__name__) + + +class zvmUtils(object): + _MAX_REGRANT_USER_NUMBER = 1000 + + def __init__(self): + self._xcat_url = xcatutils.xCatURL() + self._zhcp_userid = None + self._userid_map = {} + self._xcat_node_name = self._get_xcat_node_name() + + def get_node_from_port(self, port_id): + return self._get_nic_settings(port_id, get_node=True) + + def get_nic_ids(self): + addp = '' + url = self._xcat_url.tabdump("/switch", addp) + nic_settings = xcatutils.xcat_request("GET", url) + # remove table header + nic_settings['data'][0].pop(0) + # it's possible to return empty array + return nic_settings['data'][0] + + def _get_nic_settings(self, port_id, field=None, get_node=False): + """Get NIC information from xCat switch table.""" + LOG.debug(_("Get nic information for port: %s"), port_id) + addp = '&col=port&value=%s' % port_id + '&attribute=%s' % ( + field and field or 'node') + url = self._xcat_url.gettab("/switch", addp) + nic_settings = xcatutils.xcat_request("GET", url) + ret_value = nic_settings['data'][0][0] + if field is None and not get_node: + ret_value = self.get_userid_from_node(ret_value) + return ret_value + + def get_userid_from_node(self, node): + addp = '&col=node&value=%s&attribute=userid' % node + url = self._xcat_url.gettab("/zvm", addp) + user_info = xcatutils.xcat_request("GET", url) + return user_info['data'][0][0] + + def couple_nic_to_vswitch(self, vswitch_name, switch_port_name, + zhcp, userid, dm=True, immdt=True): + """Couple nic to vswitch.""" + LOG.debug(_("Connect nic to switch: %s"), vswitch_name) + vdev = self._get_nic_settings(switch_port_name, "interface") + if vdev: + self._couple_nic(zhcp, vswitch_name, userid, vdev, dm, immdt) + else: + raise exception.zVMInvalidDataError(msg=('Cannot get vdev for ' + 'user %s, couple to port %s') % + (userid, switch_port_name)) + return vdev + + def uncouple_nic_from_vswitch(self, vswitch_name, switch_port_name, + zhcp, userid, dm=True, immdt=True): + """Uncouple nic from vswitch.""" + LOG.debug(_("Disconnect nic from switch: %s"), vswitch_name) + vdev = self._get_nic_settings(switch_port_name, "interface") + self._uncouple_nic(zhcp, userid, vdev, dm, immdt) + + def set_vswitch_port_vlan_id(self, vlan_id, switch_port_name, vdev, zhcp, + vswitch_name): + userid = self._get_nic_settings(switch_port_name) + if not userid: + raise exception.zVMInvalidDataError(msg=('Cannot get userid by ' + 'port %s') % (switch_port_name)) + url = self._xcat_url.xdsh("/%s" % zhcp) + commands = '/opt/zhcp/bin/smcli Virtual_Network_Vswitch_Set_Extended' + commands += " -T %s" % userid + commands += ' -k grant_userid=%s' % userid + commands += " -k switch_name=%s" % vswitch_name + commands += " -k user_vlan_id=%s" % vlan_id + xdsh_commands = 'command=%s' % commands + body = [xdsh_commands] + xcatutils.xcat_request("PUT", url, body) + + def grant_user(self, zhcp, vswitch_name, userid): + """Set vswitch to grant user.""" + url = self._xcat_url.xdsh("/%s" % zhcp) + commands = '/opt/zhcp/bin/smcli Virtual_Network_Vswitch_Set_Extended' + commands += " -T %s" % userid + commands += " -k switch_name=%s" % vswitch_name + commands += " -k grant_userid=%s" % userid + xdsh_commands = 'command=%s' % commands + body = [xdsh_commands] + xcatutils.xcat_request("PUT", url, body) + + def revoke_user(self, zhcp, vswitch_name, userid): + """Set vswitch to grant user.""" + url = self._xcat_url.xdsh("/%s" % zhcp) + commands = '/opt/zhcp/bin/smcli Virtual_Network_Vswitch_Set_Extended' + commands += " -T %s" % userid + commands += " -k switch_name=%s" % vswitch_name + commands += " -k revoke_userid=%s" % userid + xdsh_commands = 'command=%s' % commands + body = [xdsh_commands] + xcatutils.xcat_request("PUT", url, body) + + def _couple_nic(self, zhcp, vswitch_name, userid, vdev, dm, immdt): + """Couple NIC to vswitch by adding vswitch into user direct.""" + url = self._xcat_url.xdsh("/%s" % zhcp) + if dm: + commands = '/opt/zhcp/bin/smcli' + commands += ' Virtual_Network_Adapter_Connect_Vswitch_DM' + commands += " -T %s " % userid + "-v %s" % vdev + commands += " -n %s" % vswitch_name + xdsh_commands = 'command=%s' % commands + body = [xdsh_commands] + xcatutils.xcat_request("PUT", url, body) + if immdt: + # the inst must be active, or this call will failed + commands = '/opt/zhcp/bin/smcli' + commands += ' Virtual_Network_Adapter_Connect_Vswitch' + commands += " -T %s " % userid + "-v %s" % vdev + commands += " -n %s" % vswitch_name + xdsh_commands = 'command=%s' % commands + body = [xdsh_commands] + xcatutils.xcat_request("PUT", url, body) + + def _uncouple_nic(self, zhcp, userid, vdev, dm, immdt): + """Couple NIC to vswitch by adding vswitch into user direct.""" + url = self._xcat_url.xdsh("/%s" % zhcp) + if dm: + commands = '/opt/zhcp/bin/smcli' + commands += ' Virtual_Network_Adapter_Disconnect_DM' + commands += " -T %s " % userid + "-v %s" % vdev + xdsh_commands = 'command=%s' % commands + body = [xdsh_commands] + xcatutils.xcat_request("PUT", url, body) + if immdt: + # the inst must be active, or this call will failed + commands = '/opt/zhcp/bin/smcli' + commands += ' Virtual_Network_Adapter_Disconnect' + commands += " -T %s " % userid + "-v %s" % vdev + xdsh_commands = 'command=%s' % commands + body = [xdsh_commands] + xcatutils.xcat_request("PUT", url, body) + + def put_user_direct_online(self, zhcp, userid): + url = self._xcat_url.xdsh("/%s" % zhcp) + commands = '/opt/zhcp/bin/smcli Static_Image_Changes_Immediate_DM' + commands += " -T %s" % userid + xdsh_commands = 'command=%s' % commands + body = [xdsh_commands] + xcatutils.xcat_request("PUT", url, body) + + def get_zhcp_userid(self, zhcp): + if not self._zhcp_userid: + self._zhcp_userid = self.get_userid_from_node(zhcp) + return self._zhcp_userid + + def add_vswitch(self, zhcp, name, rdev, + controller='*', + connection=1, queue_mem=8, router=0, network_type=2, vid=0, + port_type=1, update=1, gvrp=2, native_vid=1): + ''' + connection:0-unspecified 1-Actice 2-non-Active + router:0-unspecified 1-nonrouter 2-prirouter + type:0-unspecified 1-IP 2-ethernet + vid:1-4094 for access port defaut vlan + port_type:0-unspecified 1-access 2-trunk + update:0-unspecified 1-create 2-create and add to system + configuration file + gvrp:0-unspecified 1-gvrp 2-nogvrp + ''' + if (self._does_vswitch_exist(zhcp, name)): + LOG.info(_('Vswitch %s already exists.'), name) + return + + # if vid = 0, port_type, gvrp and native_vlanid are not + # allowed to specified + if not len(vid): + vid = 0 + port_type = 0 + gvrp = 0 + native_vid = -1 + else: + vid = str(vid[0][0]) + '-' + str(vid[0][1]) + + userid = self.get_zhcp_userid(zhcp) + url = self._xcat_url.xdsh("/%s" % zhcp) + commands = '/opt/zhcp/bin/smcli Virtual_Network_Vswitch_Create' + commands += " -T %s" % userid + commands += ' -n %s' % name + if rdev: + commands += " -r %s" % rdev.replace(',', ' ') + #commands += " -a %s" % osa_name + if controller != '*': + commands += " -i %s" % controller + commands += " -c %s" % connection + commands += " -q %s" % queue_mem + commands += " -e %s" % router + commands += " -t %s" % network_type + commands += " -v %s" % vid + commands += " -p %s" % port_type + commands += " -u %s" % update + commands += " -G %s" % gvrp + commands += " -V %s" % native_vid + xdsh_commands = 'command=%s' % commands + body = [xdsh_commands] + + result = xcatutils.xcat_request("PUT", url, body) + if (result['errorcode'][0][0] != '0') or \ + (not self._does_vswitch_exist(zhcp, name)): + raise exception.zvmException( + msg=("switch: %s add failed, %s") % + (name, result['data'][0][0])) + LOG.info(_('Created vswitch %s done.'), name) + + def _does_vswitch_exist(self, zhcp, vsw): + userid = self.get_zhcp_userid(zhcp) + url = self._xcat_url.xdsh("/%s" % zhcp) + commands = '/opt/zhcp/bin/smcli Virtual_Network_Vswitch_Query' + commands += " -T %s" % userid + commands += " -s %s" % vsw + xdsh_commands = 'command=%s' % commands + body = [xdsh_commands] + result = xcatutils.xcat_request("PUT", url, body) + + return (result['errorcode'][0][0] == '0') + + def re_grant_user(self, zhcp): + """Grant user again after z/VM is re-IPLed""" + ports_info = self._get_userid_vswitch_vlan_id_mapping(zhcp) + records_num = 0 + cmd = '' + + def run_command(command): + xdsh_commands = 'command=%s' % command + body = [xdsh_commands] + url = self._xcat_url.xdsh("/%s" % zhcp) + xcatutils.xcat_request("PUT", url, body) + + for (port_id, port) in ports_info.items(): + if port['userid'] is None or port['vswitch'] is None: + continue + if len(port['userid']) == 0 or len(port['vswitch']) == 0: + continue + + cmd += '/opt/zhcp/bin/smcli ' + cmd += 'Virtual_Network_Vswitch_Set_Extended ' + cmd += '-T %s ' % port['userid'] + cmd += '-k switch_name=%s ' % port['vswitch'] + cmd += '-k grant_userid=%s' % port['userid'] + try: + if int(port['vlan_id']) in range(1, 4094): + cmd += ' -k user_vlan_id=%s\n' % port['vlan_id'] + else: + cmd += '\n' + except ValueError: + # just in case there are bad records of vlan info which + # could be a string + LOG.warn(_("Unknown vlan '%(vlan)s' for user %(user)s."), + {'vlan': port['vlan_id'], 'user': port['userid']}) + cmd += '\n' + continue + records_num += 1 + if records_num >= self._MAX_REGRANT_USER_NUMBER: + try: + commands = 'echo -e "#!/bin/sh\n%s" > grant.sh' % cmd[:-1] + run_command(commands) + commands = 'sh grant.sh;rm -f grant.sh' + run_command(commands) + records_num = 0 + cmd = '' + except Exception: + LOG.warn(_("Grant user failed")) + + if len(cmd) > 0: + commands = 'echo -e "#!/bin/sh\n%s" > grant.sh' % cmd[:-1] + run_command(commands) + commands = 'sh grant.sh;rm -f grant.sh' + run_command(commands) + return ports_info + + def _get_userid_vswitch_vlan_id_mapping(self, zhcp): + ports_info = self.get_nic_ids() + ports = {} + for p in ports_info: + port_info = p.split(',') + target_host = port_info[5].strip('"') + port_vid = port_info[3].strip('"') + port_id = port_info[2].strip('"') + vswitch = port_info[1].strip('"') + nodename = port_info[0].strip('"') + if target_host == zhcp: + ports[port_id] = {'nodename': nodename, + 'vswitch': vswitch, + 'userid': None, + 'vlan_id': port_vid} + + def get_all_userid(): + users = {} + addp = '' + url = self._xcat_url.tabdump("/zvm", addp) + all_userids = xcatutils.xcat_request("GET", url) + header = '#node,hcp,userid,nodetype,parent,comments,disable' + all_userids['data'][0].remove(header) + if len(all_userids) > 0: + for u in all_userids['data'][0]: + user_info = u.split(',') + userid = user_info[2].strip('"') + nodename = user_info[0].strip('"') + users[nodename] = {'userid': userid} + + return users + + users = get_all_userid() + + for (port_id, port) in ports.items(): + try: + ports[port_id]['userid'] = users[port['nodename']]['userid'] + except Exception: + LOG.info(_("Garbage port found. port id: %s") % port_id) + + return ports + + def update_xcat_switch(self, port, vswitch, vlan): + """Update information in xCAT switch table.""" + commands = "port=%s" % port + commands += " switch.switch=%s" % vswitch + commands += " switch.vlan=%s" % (vlan and vlan or -1) + url = self._xcat_url.tabch("/switch") + body = [commands] + xcatutils.xcat_request("PUT", url, body) + + def create_xcat_mgt_network(self, zhcp, mgt_ip, mgt_mask, mgt_vswitch): + url = self._xcat_url.xdsh("/%s" % zhcp) + xdsh_commands = ('command=smcli Virtual_Network_Adapter_Query' + ' -T %s -v 0800') % self._xcat_node_name + body = [xdsh_commands] + result = xcatutils.xcat_request("PUT", url, body)['data'][0][0] + code = result.split("\n") + # return code 212: Adapter does not exist + new_nic = '' + if len(code) == 4 and code[1].split(': ')[2] == '212': + new_nic = ('vmcp define nic 0800 type qdio\n' + + 'vmcp couple 0800 system %s\n' % (mgt_vswitch)) + elif len(code) == 7: + status = code[4].split(': ')[2] + if status == 'Coupled and active': + # we just assign the IP/mask, + # no matter if it is assigned or not + LOG.info(_("Assign IP for NIC 800.")) + else: + LOG.error(_("NIC 800 staus is unknown.")) + return + else: + raise exception.zvmException( + msg="Unknown information from SMAPI") + + url = self._xcat_url.xdsh("/%s") % self._xcat_node_name + cmd = new_nic + ('/usr/bin/perl /usr/sbin/sspqeth2.pl ' + + '-a %s -d 0800 0801 0802 -e eth2 -m %s -g %s' + % (mgt_ip, mgt_mask, mgt_ip)) + xdsh_commands = 'command=%s' % cmd + body = [xdsh_commands] + xcatutils.xcat_request("PUT", url, body) + + def _get_xcat_node_ip(self): + addp = '&col=key&value=master&attribute=value' + url = self._xcat_url.gettab("/site", addp) + return xcatutils.xcat_request("GET", url)['data'][0][0] + + def _get_xcat_node_name(self): + xcat_ip = self._get_xcat_node_ip() + addp = '&col=ip&value=%s&attribute=node' % (xcat_ip) + url = self._xcat_url.gettab("/hosts", addp) + return (xcatutils.xcat_request("GET", url)['data'][0][0]) + + def query_xcat_uptime(self, zhcp): + url = self._xcat_url.xdsh("/%s" % zhcp) + cmd = '/opt/zhcp/bin/smcli Image_Query_Activate_Time' + cmd += " -T %s" % self.get_userid_from_node( + self._xcat_node_name) + # format 4: yyyy-mm-dd + cmd += " -f %s" % "4" + xdsh_commands = 'command=%s' % cmd + body = [xdsh_commands] + ret_str = xcatutils.xcat_request("PUT", url, body)['data'][0][0] + return ret_str.split('on ')[1] + + def query_zvm_uptime(self, zhcp): + url = self._xcat_url.xdsh("/%s" % zhcp) + cmd = '/opt/zhcp/bin/smcli System_Info_Query' + xdsh_commands = 'command=%s' % cmd + body = [xdsh_commands] + ret_str = xcatutils.xcat_request("PUT", url, body)['data'][0][0] + return ret_str.split('\n')[4].split(': ', 3)[2] diff --git a/neutron-zvm-plugin/neutron/plugins/zvm/common/xcatutils.py b/neutron-zvm-plugin/neutron/plugins/zvm/common/xcatutils.py new file mode 100755 index 0000000..e881472 --- /dev/null +++ b/neutron-zvm-plugin/neutron/plugins/zvm/common/xcatutils.py @@ -0,0 +1,205 @@ +# vim: tabstop=4 shiftwidth=4 softtabstop=4 + +# Copyright 2014 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 functools +import httplib + +from neutron.openstack.common import jsonutils +from neutron.openstack.common.gettextutils import _ +from neutron.openstack.common import log as logging +from neutron.plugins.zvm.common import config +from neutron.plugins.zvm.common import constants +from neutron.plugins.zvm.common import exception + +LOG = logging.getLogger(__name__) +CONF = config.CONF + + +class xCatURL(object): + """To return xCat url for invoking xCat REST API.""" + + def __init__(self): + """Set constant that used to form xCat url.""" + self.PREFIX = '/xcatws' + self.SUFFIX = '?userName=' + CONF.AGENT.zvm_xcat_username + \ + '&password=' + CONF.AGENT.zvm_xcat_password + \ + '&format=json' + + self.NODES = '/nodes' + self.TABLES = '/tables' + self.XDSH = '/dsh' + + def tabdump(self, arg='', addp=None): + rurl = self.PREFIX + self.TABLES + arg + self.SUFFIX + return self._append_addp(rurl, addp) + + def _append_addp(self, rurl, addp=None): + if addp is not None: + return rurl + addp + else: + return rurl + + def gettab(self, arg='', addp=None): + """Get table arg, with attribute addp.""" + rurl = self.PREFIX + self.TABLES + arg + self.SUFFIX + return self._append_addp(rurl, addp) + + def tabch(self, arg='', addp=None): + """Add/update/delete row(s) in table arg, with attribute addp.""" + rurl = self.PREFIX + self.TABLES + arg + self.SUFFIX + return self._append_addp(rurl, addp) + + def xdsh(self, arg=''): + """Run shell command.""" + return self.PREFIX + self.NODES + arg + self.XDSH + self.SUFFIX + + +class xCatConnection(): + """Https requests to xCat web service.""" + def __init__(self): + """Initialize https connection to xCat service.""" + self.host = CONF.AGENT.zvm_xcat_server + self.xcat_timeout = CONF.AGENT.zvm_xcat_timeout + try: + self.conn = httplib.HTTPSConnection(self.host, None, None, None, + True, self.xcat_timeout) + except Exception: + LOG.error(_("Connect to xCat server %s failed") % self.host) + raise exception.zVMxCatConnectionFailed(xcatserver=self.host) + + def request(self, method, url, body=None, headers={}): + """Do http request to xCat server + + Will return (response_status, response_reason, response_body) + """ + if body is not None: + body = jsonutils.dumps(body) + headers = {'content-type': 'text/plain', + 'content-length': len(body)} + + try: + self.conn.request(method, url, body, headers) + except Exception as err: + LOG.error(_("Request to xCat server %(host)s failed: %(err)s") % + {'host': self.host, 'err': err}) + raise exception.zVMxCatRequestFailed(xcatserver=self.host, + err=err) + + res = self.conn.getresponse() + msg = res.read() + resp = { + 'status': res.status, + 'reason': res.reason, + 'message': msg} + + # NOTE(rui): Currently, only xCat returns 200 or 201 can be + # considered acceptable. + 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: + LOG.error(_("Request to xCat server %(host)s failed: %(err)s") % + {'host': self.host, 'err': err}) + raise exception.zVMxCatRequestFailed(xcatserver=self.host, + err=err) + + 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: + LOG.error(_("Respone is not in JSON format")) + raise exception.zVMJsonLoadsError() + + +def wrap_invalid_xcat_resp_data_error(function): + """zVM driver get zVM hypervisor and virtual machine information + from xCat. xCat REST API response has its own fixed format(a JSON + stream). zVM driver abstract useful info base on the special format, + and raise exception if the data in incorrect format. + """ + @functools.wraps(function) + def decorated_function(*arg, **kwargs): + try: + return function(*arg, **kwargs) + except (ValueError, TypeError, IndexError) as err: + LOG.error(_('Invalid data returned from xCat: %s') % err) + raise exception.zVMInvalidxCatResponseDataError(msg=err) + except Exception as err: + raise + + return decorated_function + + +@wrap_invalid_xcat_resp_data_error +def load_xcat_resp(message): + """Abstract information from xCat REST response body. + + As default, xCat response will in format of JSON and can be + converted to Python dictionary, would looks like: + {"data": [{"info": [info,]}, {"data": [data,]}, ..., {"error": [error,]}]} + + Returns a Python dictionary, looks like: + {'info': [info,], + 'data': [data,], + 'error': [error,]} + """ + resp_list = jsonloads(message)['data'] + keys = constants.XCAT_RESPONSE_KEYS + + resp = {} + try: + 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)) + except Exception: + LOG.error(_("Invalid data returned from xCat: %s") % message) + raise exception.zVMInvalidxCatResponseDataError(msg=message) + + if not verify_xcat_resp(resp): + LOG.error(_("Error returned from xCAT: %s") % message) + raise exception.zVMInvalidxCatResponseDataError(msg=message) + else: + return resp + + +@wrap_invalid_xcat_resp_data_error +def verify_xcat_resp(resp_dict): + """Check whether xCAT REST API response contains an error.""" + if resp_dict.get('error'): + if resp_dict['error'][0][0].find('Warning'): + return True + return False + else: + return True diff --git a/neutron-zvm-plugin/neutron/tests/unit/zvm/test_zvm_network.py b/neutron-zvm-plugin/neutron/tests/unit/zvm/test_zvm_network.py new file mode 100755 index 0000000..3b2ddd0 --- /dev/null +++ b/neutron-zvm-plugin/neutron/tests/unit/zvm/test_zvm_network.py @@ -0,0 +1,48 @@ +# vim: tabstop=4 shiftwidth=4 softtabstop=4 + +# Copyright 2014 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. + +""" +Unit tests for the z/VM network. +""" + +import mock + +from oslo.config import cfg +from neutron.plugins.zvm.agent import zvm_network +from neutron.tests import base + +FLAT_NETWORKS = ['flat_net1'] +VLAN_NETWORKS = ['vlan_net1:100:500'] +NETWORK_MAPS = {'vlan_net1': [(100, 500)], 'flat_net1': []} + + +class TestZVMNetwork(base.BaseTestCase): + + _FAKE_NETWORK_VLAN_RANGES = "fakevsw1:1:4094,fakevsw2,fakevsw3:2:2999" + + def setUp(self): + super(TestZVMNetwork, self).setUp() + cfg.CONF.set_override('flat_networks', FLAT_NETWORKS, + group='ml2_type_flat') + cfg.CONF.set_override('network_vlan_ranges', VLAN_NETWORKS, + group='ml2_type_vlan') + + with mock.patch('neutron.plugins.zvm.common.utils.zvmUtils') as utils: + self._zvm_network = zvm_network.zvmNetwork() + + def test_get_network_maps(self): + maps = self._zvm_network.get_network_maps() + self.assertEqual(maps, NETWORK_MAPS) diff --git a/neutron-zvm-plugin/neutron/tests/unit/zvm/test_zvm_neutron_agent.py b/neutron-zvm-plugin/neutron/tests/unit/zvm/test_zvm_neutron_agent.py new file mode 100755 index 0000000..a33cb4a --- /dev/null +++ b/neutron-zvm-plugin/neutron/tests/unit/zvm/test_zvm_neutron_agent.py @@ -0,0 +1,227 @@ +# vim: tabstop=4 shiftwidth=4 softtabstop=4 + +# Copyright 2014 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. + +""" +Unit tests for neutron z/VM driver +""" + +import mock + +from neutron.plugins.zvm.agent import zvm_neutron_agent +from neutron.tests import base +from oslo.config import cfg + +FLAT_NETWORKS = ['flat_net1'] +VLAN_NETWORKS = ['vlan_net1:100:500'] +NET_UUID = 'zvm-net-uuid' +PORT_UUID = 'zvm-port-uuid' + + +class FakeLoopingCall(object): + def __init__(self, fake_time): + self.fake_time = fake_time + + def start(self, interval=0): + self.fake_time() + + +class TestZVMNeutronAgent(base.BaseTestCase): + + def setUp(self): + super(TestZVMNeutronAgent, self).setUp() + self.addCleanup(cfg.CONF.reset) + cfg.CONF.set_override('rpc_backend', + 'neutron.openstack.common.rpc.impl_fake') + cfg.CONF.set_override('flat_networks', FLAT_NETWORKS, + group='ml2_type_flat') + cfg.CONF.set_override('network_vlan_ranges', VLAN_NETWORKS, + group='ml2_type_vlan') + + mock.patch('neutron.openstack.common.loopingcall.' + 'FixedIntervalLoopingCall', + new=FakeLoopingCall) + with mock.patch( + 'neutron.plugins.zvm.common.utils.zvmUtils') as mock_Utils: + instance = mock_Utils.return_value + get_zhcp_userid = mock.MagicMock(return_value='zhcp_user') + create_xcat_mgt_network = mock.MagicMock() + + instance.get_zhcp_userid = get_zhcp_userid + instance.create_xcat_mgt_network = create_xcat_mgt_network + net_attrs = {'fake_uuid1': { + 'vswitch': 'fake_vsw', 'userid': 'fake_user1'}} + instance.re_grant_user = mock.MagicMock(return_value=net_attrs) + instance.query_xcat_uptime = mock.MagicMock( + return_value="xcat uptime 1") + instance.query_zvm_uptime = mock.MagicMock( + return_value="zvm uptime 1") + + self.agent = zvm_neutron_agent.zvmNeutronAgent() + self.agent.plugin_rpc = mock.Mock() + self.agent.context = mock.Mock() + self.agent.agent_id = mock.Mock() + + def test_port_bound_vlan(self): + vid = 100 + with mock.patch.object(zvm_neutron_agent, "LOG") as log: + self._test_port_bound('vlan', vid) + log.info.assert_called_with('Binding VLAN, VLAN ID: %s', vid) + + def test_port_bound_flat(self): + with mock.patch.object(zvm_neutron_agent, "LOG") as log: + self._test_port_bound('flat') + log.info.assert_called_with('Bind %s mode done', 'flat') + + def _test_port_bound(self, network_type, vid=None): + port = mock.MagicMock() + net_uuid = NET_UUID + mock_enable_vlan = mock.MagicMock() + enable_vlan = False + + if network_type == 'vlan': + enable_vlan = True + + with mock.patch.multiple( + self.agent._utils, + couple_nic_to_vswitch=mock.MagicMock(), + put_user_direct_online=mock.MagicMock(), + set_vswitch_port_vlan_id=mock_enable_vlan): + + self.agent.port_bound(port, net_uuid, network_type, None, + vid, 'fake_user') + + self.assertEqual(enable_vlan, mock_enable_vlan.called) + + def test_port_unbound(self): + # port_unbound just call utils.revoke_user, revoke_user is covered + # in test_zvm_utils + pass + + def test_treat_devices_added_returns_true_for_missing_device(self): + attrs = {'get_device_details.side_effect': Exception()} + self.agent.plugin_rpc.configure_mock(**attrs) + # no exception should be raised + self.agent._treat_devices_added([]) + + def test_treat_devices_added_down_port(self): + details = dict(port_id='added_port_down', physical_network='vsw', + segmentation_id='10', network_id='fake_net', + network_type='flat', admin_state_up=False) + attrs = {'get_device_details.return_value': details} + self.agent.plugin_rpc.configure_mock(**attrs) + with mock.patch.object(self.agent, "_treat_vif_port", + mock.Mock(return_value=('fake_node', 'fake_user'))): + self.agent._treat_devices_added(['added_port_down']) + self.assertTrue(self.agent.plugin_rpc.update_device_down.called) + + def test_treat_devices_added_up_port(self): + details = dict(port_id='added_port', physical_network='vsw', + segmentation_id='10', network_id='fake_net', + network_type='flat', admin_state_up=True) + attrs = {'get_device_details.return_value': details} + self.agent.plugin_rpc.configure_mock(**attrs) + with mock.patch.object(self.agent, "_treat_vif_port", + mock.Mock(return_value=('fake_node', 'fake_user'))): + self.agent._treat_devices_added(['added_port']) + self.assertTrue(self.agent.plugin_rpc.get_device_details.called) + + def test_treat_devices_added_missing_port_id(self): + details = mock.MagicMock() + details.__contains__.side_effect = lambda x: False + attrs = {'get_device_details.return_value': details} + self.agent.plugin_rpc.configure_mock(**attrs) + with mock.patch.object(zvm_neutron_agent, "LOG") as log: + self.agent._treat_devices_added(['unknown_port']) + log.debug.assert_called_with( + "Device %s not defined on Neutron server", "unknown_port") + + def test_treat_devices_removed_returns_true_for_missing_device(self): + attrs = {'update_device_down.side_effect': Exception()} + self.agent.plugin_rpc.configure_mock(**attrs) + devices = ['fake_uuid1'] + with mock.patch.object(zvm_neutron_agent, "LOG") as log: + self.agent._treat_devices_removed(devices) + self.assertTrue(log.exception.called) + + def test_treat_devices_removed(self): + devices = ['unknown_port', 'fake_uuid1'] + with mock.patch.object(zvm_neutron_agent, "LOG") as log: + self.agent._treat_devices_removed(devices) + log.warn.assert_called_with('Can\'t find port %s in zvm agent', + 'unknown_port') + self.assertTrue(self.agent.plugin_rpc.update_device_down.called) + + def test_port_update_up(self): + with mock.patch.object(self.agent.plugin_rpc, + "update_device_up") as rpc: + with mock.patch.object(self.agent._utils, + "couple_nic_to_vswitch") as couple: + self.agent.port_update(None, port={'id': 'fake_uuid1', + 'admin_state_up': True}) + self.assertTrue(rpc.called) + self.assertTrue(couple.called) + + def test_port_update_down(self): + with mock.patch.object(self.agent.plugin_rpc, + "update_device_down") as rpc: + with mock.patch.object(self.agent._utils, + "uncouple_nic_from_vswitch") as couple: + self.agent.port_update(None, port={'id': 'fake_uuid1', + 'admin_state_up': False}) + self.assertTrue(rpc.called) + self.assertTrue(couple.called) + + # Test agent state report + def test_report_state(self): + with mock.patch.object(self.agent.state_rpc, + "report_state") as report_st: + self.agent._report_state() + report_st.assert_called_with(self.agent.context, + self.agent.agent_state) + self.assertNotIn("start_flag", self.agent.agent_state) + + def test_treat_vif_port(self): + with mock.patch.object(self.agent, "port_bound") as bound: + self.agent._treat_vif_port('port_id', 'network_id', 'flat', + 'vsw1', '10', True) + self.assertTrue(bound.called) + + self.agent._treat_vif_port('port_id', 'network_id', 'flat', + 'vsw1', '10', False) + self.assertTrue(self.agent._utils.grant_user.called) + + def test_handle_restar_zvm(self): + q_xcat = mock.MagicMock(return_value="xcat uptime 2") + q_zvm = mock.MagicMock(return_value="zvm uptime 2") + re_grant = mock.MagicMock() + + with mock.patch.multiple( + self.agent._utils, + query_xcat_uptime=q_xcat, + query_zvm_uptime=q_zvm, + re_grant_user=re_grant): + self.agent._restart_handler.send(None) + self.assertTrue(re_grant.called) + self.assertTrue(self.agent._utils.create_xcat_mgt_network.called) + + def test_handle_restar_zvm_exception(self): + q_xcat = mock.MagicMock(side_effect= + Exception("xcat uptime exception")) + with mock.patch.object(zvm_neutron_agent, "LOG") as log: + with mock.patch.object(self.agent._utils, + "query_xcat_uptime", q_xcat): + self.agent._restart_handler.send(None) + log.exception.assert_called_with("Failed to handle restart") diff --git a/neutron-zvm-plugin/neutron/tests/unit/zvm/test_zvm_utils.py b/neutron-zvm-plugin/neutron/tests/unit/zvm/test_zvm_utils.py new file mode 100755 index 0000000..70e0f8c --- /dev/null +++ b/neutron-zvm-plugin/neutron/tests/unit/zvm/test_zvm_utils.py @@ -0,0 +1,471 @@ +# vim: tabstop=4 shiftwidth=4 softtabstop=4 + +# Copyright 2014 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. + +""" +Unit tests for the z/VM utils. +""" + +import mock + +from neutron.plugins.zvm.common import exception +from neutron.plugins.zvm.common import utils +from neutron.tests import base +from oslo.config import cfg + + +class TestZVMUtils(base.BaseTestCase): + + _FAKE_VSWITCH_NAME = "fakevsw1" + _FAKE_PORT_NAME = "fake_port_name" + _FAKE_RET_VAL = 0 + _FAKE_VM_PATH = "fake_vm_path" + _FAKE_VSWITCH = "fakevsw1" + _FAKE_VLAN_ID = "fake_vlan_id" + _FAKE_ZHCP_NODENAME = "fakezhcp" + _FAKE_ZHCP_USER = 'zhcp_user' + _FAKE_VDEV = "1000" + _FAKE_XCAT_NODENAME = "fakexcat" + _FAKE_XCAT_USER = "fake_xcat_user" + _FAKE_XCAT_PW = "fake_xcat_password" + + def setUp(self): + super(TestZVMUtils, self).setUp() + self.addCleanup(cfg.CONF.reset) + cfg.CONF.set_override('zvm_xcat_username', self._FAKE_XCAT_USER, + group='AGENT') + cfg.CONF.set_override('zvm_xcat_password', self._FAKE_XCAT_PW, + group='AGENT') + with mock.patch( + 'neutron.plugins.zvm.common.utils.zvmUtils._get_xcat_node_name', + mock.Mock(return_value=self._FAKE_XCAT_NODENAME)): + self._utils = utils.zvmUtils() + + def test_couple_nic_to_vswitch(self): + xcat_req = mock.Mock() + xcat_req.side_effect = [{'data': [[self._FAKE_VDEV]]}, + {'data': [['OK']]}, + {'data': [['OK']]}] + with mock.patch('neutron.plugins.zvm.common.xcatutils.xcat_request', + xcat_req): + ret = self._utils.couple_nic_to_vswitch(self._FAKE_VSWITCH, + self._FAKE_PORT_NAME, + self._FAKE_ZHCP_NODENAME, + "fake_user") + self.assertEqual(ret, self._FAKE_VDEV) + + url_vdev = ('/xcatws/tables/switch?userName=fake_xcat_user&' + 'password=fake_xcat_password&format=json&' + 'col=port&value=fake_port_name&attribute=interface') + url_couple_nic = ('/xcatws/nodes/fakezhcp/dsh?userName=' + 'fake_xcat_user&password=fake_xcat_password&format=json') + body_couple_nic_dm = [('command=/opt/zhcp/bin/smcli' + ' Virtual_Network_Adapter_Connect_Vswitch_DM -T fake_user' + ' -v 1000 -n fakevsw1')] + body_couple_nic = [('command=/opt/zhcp/bin/smcli' + ' Virtual_Network_Adapter_Connect_Vswitch -T fake_user' + ' -v 1000 -n fakevsw1')] + + calls = [mock.call('GET', url_vdev), + mock.call('PUT', url_couple_nic, body_couple_nic_dm), + mock.call('PUT', url_couple_nic, body_couple_nic)] + xcat_req.assert_has_calls(calls) + + def test_grant_user(self): + xcat_req = mock.Mock() + with mock.patch('neutron.plugins.zvm.common.xcatutils.xcat_request', + xcat_req): + ret = self._utils.grant_user(self._FAKE_ZHCP_NODENAME, + self._FAKE_VSWITCH, + "fake_user") + url_grant_user = ('/xcatws/nodes/fakezhcp/dsh?userName=' + 'fake_xcat_user&password=fake_xcat_password&format=json') + body_grant_user = [('command=/opt/zhcp/bin/smcli' + ' Virtual_Network_Vswitch_Set_Extended -T fake_user' + ' -k switch_name=fakevsw1 -k grant_userid=fake_user')] + xcat_req.assert_called_with('PUT', url_grant_user, body_grant_user) + + def test_uncouple_nic_from_vswitch(self): + xcat_req = mock.Mock() + xcat_req.side_effect = [{'data': [[self._FAKE_VDEV]]}, + {'data': [['OK']]}, + {'data': [['OK']]}] + with mock.patch('neutron.plugins.zvm.common.xcatutils.xcat_request', + xcat_req): + ret = self._utils.uncouple_nic_from_vswitch(self._FAKE_VSWITCH, + self._FAKE_PORT_NAME, + self._FAKE_ZHCP_NODENAME, + "fake_user") + + url_vdev = ('/xcatws/tables/switch?userName=fake_xcat_user&' + 'password=fake_xcat_password&format=json&' + 'col=port&value=fake_port_name&attribute=interface') + url_uncouple_nic = ('/xcatws/nodes/fakezhcp/dsh?userName=' + 'fake_xcat_user&password=fake_xcat_password&format=json') + body_uncouple_nic_dm = [('command=/opt/zhcp/bin/smcli' + ' Virtual_Network_Adapter_Disconnect_DM -T fake_user' + ' -v 1000')] + body_uncouple_nic = [('command=/opt/zhcp/bin/smcli' + ' Virtual_Network_Adapter_Disconnect -T fake_user -v 1000')] + + calls = [mock.call('GET', url_vdev), + mock.call('PUT', url_uncouple_nic, body_uncouple_nic_dm), + mock.call('PUT', url_uncouple_nic, body_uncouple_nic)] + xcat_req.assert_has_calls(calls) + + def test_revoke_user(self): + res = {'errorcode': [['0']]} + xcat_req = mock.MagicMock() + xcat_req.return_value = res + with mock.patch('neutron.plugins.zvm.common.xcatutils.xcat_request', + xcat_req): + self._utils.revoke_user(self._FAKE_ZHCP_NODENAME, + self._FAKE_VSWITCH_NAME, + "fake_user") + url_revoke_user = ('/xcatws/nodes/fakezhcp/dsh?userName=' + 'fake_xcat_user&password=fake_xcat_password&format=json') + body_revoke_user = [('command=/opt/zhcp/bin/smcli' + ' Virtual_Network_Vswitch_Set_Extended -T fake_user' + ' -k switch_name=fakevsw1 -k revoke_userid=fake_user')] + xcat_req.assert_called_with('PUT', url_revoke_user, + body_revoke_user) + + def test_add_vswitch_exist(self): + res = {'errorcode': [['0']]} + xcat_req = mock.MagicMock() + xcat_req.return_value = res + self._utils.get_zhcp_userid = mock.MagicMock( + return_value=self._FAKE_ZHCP_USER) + with mock.patch('neutron.plugins.zvm.common.xcatutils.xcat_request', + xcat_req): + with mock.patch.object(utils, "LOG") as log: + self._utils.add_vswitch(self._FAKE_ZHCP_NODENAME, + self._FAKE_VSWITCH_NAME, + self._FAKE_VDEV) + log.info.assert_called_with('Vswitch %s already exists.', + self._FAKE_VSWITCH_NAME) + + def test_add_vswitch(self): + self._utils.get_zhcp_userid = mock.MagicMock() + self._utils.get_zhcp_userid.side_effect = [self._FAKE_ZHCP_USER, + self._FAKE_ZHCP_USER, + self._FAKE_ZHCP_USER] + xcat_req = mock.Mock() + res = {'errorcode': [['0']]} # vswitch does exist + res_err = {'errorcode': [['1']]} # vswitch does not exist + xcat_req.side_effect = [res_err, res, res] + with mock.patch('neutron.plugins.zvm.common.xcatutils.xcat_request', + xcat_req): + self._utils.add_vswitch(self._FAKE_ZHCP_NODENAME, + self._FAKE_VSWITCH_NAME, + self._FAKE_VDEV, + vid=[self._FAKE_VLAN_ID]) + url = ('/xcatws/nodes/fakezhcp/dsh?userName=fake_xcat_user' + '&password=fake_xcat_password&format=json') + body = [('command=/opt/zhcp/bin/smcli' + ' Virtual_Network_Vswitch_Create -T zhcp_user -n fakevsw1' + ' -r 1000 -c 1 -q 8 -e 0 -t 2 -v 1' + ' -p 1 -u 1 -G 2 -V 1')] + xcat_req.assert_any_called('PUT', url, body) + + def test_set_vswitch_port_vlan_id(self): + self._utils._get_nic_settings = mock.MagicMock(return_value='inst1') + xcat_req = mock.Mock() + xcat_req.return_value = "OK" + with mock.patch('neutron.plugins.zvm.common.xcatutils.xcat_request', + xcat_req): + self._utils.set_vswitch_port_vlan_id(self._FAKE_VLAN_ID, + self._FAKE_PORT_NAME, + self._FAKE_VDEV, + self._FAKE_ZHCP_NODENAME, + self._FAKE_VSWITCH) + url = ('/xcatws/nodes/fakezhcp/dsh?userName=fake_xcat_user' + '&password=fake_xcat_password&format=json') + body = [('command=/opt/zhcp/bin/smcli' + ' Virtual_Network_Vswitch_Set_Extended -T inst1' + ' -k grant_userid=inst1 -k switch_name=fakevsw1' + ' -k user_vlan_id=fake_vlan_id')] + xcat_req.assert_called_with('PUT', url, body) + + def test_get_nic_ids(self): + xcat_req = mock.Mock() + data = 'fnode,fswitch,fport,fvlan,finf,-,false' + xcat_req.return_value = {'data': [[( + '#node,switch,port,vlan,interface,comments,disable'), data]]} + with mock.patch('neutron.plugins.zvm.common.xcatutils.xcat_request', + xcat_req): + ret = self._utils.get_nic_ids() + self.assertEqual(ret, [data]) + url = ('/xcatws/tables/switch?userName=fake_xcat_user&' + 'password=fake_xcat_password&format=json') + xcat_req.assert_called_with('GET', url) + + def test_get_node_from_port(self): + xcat_req = mock.Mock() + xcat_req.side_effect = [{'data': [[self._FAKE_ZHCP_NODENAME]]}] + with mock.patch('neutron.plugins.zvm.common.xcatutils.xcat_request', + xcat_req): + ret = self._utils.get_node_from_port(self._FAKE_PORT_NAME) + self.assertEqual(ret, self._FAKE_ZHCP_NODENAME) + url = ('/xcatws/tables/switch?userName=fake_xcat_user&' + 'password=fake_xcat_password&format=json&' + 'col=port&value=fake_port_name&attribute=node') + calls = [mock.call('GET', url)] + xcat_req.assert_has_calls(calls) + + def _test_get_userid_from_node(self, node, user): + xcat_req = mock.Mock() + xcat_req.return_value = {'data': [[user]]} + with mock.patch('neutron.plugins.zvm.common.xcatutils.xcat_request', + xcat_req): + ret = self._utils.get_zhcp_userid(self._FAKE_ZHCP_NODENAME) + url = ('/xcatws/tables/zvm?userName=fake_xcat_user&' + 'password=fake_xcat_password&format=json&col=node&' + 'value=%s&attribute=userid' % node) + xcat_req.assert_called_with('GET', url) + return ret + + def test_get_userid_from_node(self): + self.assertEqual(self._test_get_userid_from_node( + self._FAKE_ZHCP_NODENAME, + self._FAKE_ZHCP_USER), + self._FAKE_ZHCP_USER) + + def test_get_zhcp_userid(self): + self.assertEqual(self._test_get_userid_from_node( + self._FAKE_ZHCP_NODENAME, + self._FAKE_ZHCP_USER), + self._FAKE_ZHCP_USER) + + def test_put_user_direct_online(self): + xcat_req = mock.Mock() + with mock.patch('neutron.plugins.zvm.common.xcatutils.xcat_request', + xcat_req): + self._utils.put_user_direct_online(self._FAKE_ZHCP_NODENAME, + 'inst1') + url = ('/xcatws/nodes/fakezhcp/dsh?userName=fake_xcat_user&' + 'password=fake_xcat_password&format=json') + body = [('command=/opt/zhcp/bin/smcli' + ' Static_Image_Changes_Immediate_DM -T inst1')] + xcat_req.assert_called_with('PUT', url, body) + + def test_update_xcat_switch(self): + xcat_req = mock.Mock() + with mock.patch('neutron.plugins.zvm.common.xcatutils.xcat_request', + xcat_req): + self._utils.update_xcat_switch(self._FAKE_PORT_NAME, + self._FAKE_VSWITCH, + self._FAKE_VLAN_ID) + url = ('/xcatws/tables/switch?userName=fake_xcat_user&' + 'password=fake_xcat_password&format=json') + body = ['port=fake_port_name switch.switch=fakevsw1' + ' switch.vlan=fake_vlan_id'] + xcat_req.assert_called_with('PUT', url, body) + + def _verify_query_nic(self, result, xcat_req): + url = ('/xcatws/nodes/fakexcat/dsh?userName=fake_xcat_user&' + 'password=fake_xcat_password&format=json') + body = ['command=smcli Virtual_Network_Adapter_Query' + ' -T fakexcat -v 0800'] + xcat_req.assert_any_with('PUT', url, body) + + def test_create_xcat_mgt_network_exist(self): + nic_def = ['zhcp: Adapter:\nzhcp: Address: 0800\n' + 'zhcp: Device count: 3\nzhcp: Adapter type: QDIO\n' + 'zhcp: Adapter status: Coupled and active\n' + 'zhcp: LAN owner: SYSTEM\n' + 'zhcp: LAN name: XCATVSW2'] + xcat_req = mock.Mock() + xcat_req.side_effect = [{'data': [nic_def]}, + {'data': [['OK']]}] + with mock.patch('neutron.plugins.zvm.common.xcatutils.xcat_request', + xcat_req): + self._utils.create_xcat_mgt_network(self._FAKE_ZHCP_NODENAME, + "10.1.1.1", + "255.255.0.0", + self._FAKE_VSWITCH) + self._verify_query_nic(nic_def, xcat_req) + url = ('/xcatws/nodes/fakexcat/dsh?userName=fake_xcat_user&' + 'password=fake_xcat_password&format=json') + body = ['command=/usr/bin/perl /usr/sbin/sspqeth2.pl -a 10.1.1.1' + ' -d 0800 0801 0802 -e eth2 -m 255.255.0.0 -g 10.1.1.1'] + xcat_req.assert_called_with('PUT', url, body) + + def test_create_xcat_mgt_network_not_exist(self): + nic_undef = ['zhcp: Failed\nzhcp: Return Code: 212\n' + 'zhcp: Reason Code: 8\n' + 'zhcp: Description: Adapter does not exist'] + xcat_req = mock.Mock() + xcat_req.side_effect = [{'data': [nic_undef]}, + {'data': [['OK']]}] + with mock.patch('neutron.plugins.zvm.common.xcatutils.xcat_request', + xcat_req): + self._utils.create_xcat_mgt_network(self._FAKE_ZHCP_NODENAME, + "10.1.1.1", + "255.255.0.0", + self._FAKE_VSWITCH) + self._verify_query_nic(nic_undef, xcat_req) + url = ('/xcatws/nodes/fakexcat/dsh?userName=fake_xcat_user&' + 'password=fake_xcat_password&format=json') + body = ['command=vmcp define nic 0800 type qdio\n' + 'vmcp couple 0800 system fakevsw1\n' + '/usr/bin/perl /usr/sbin/sspqeth2.pl -a 10.1.1.1' + ' -d 0800 0801 0802 -e eth2 -m 255.255.0.0 -g 10.1.1.1'] + xcat_req.assert_called_with('PUT', url, body) + + def test_create_xcat_mgt_network_error(self): + nic_err = ['zhcp: Adapter:\nzhcp: Address: 0800\n' + 'zhcp: Device count: 3\nzhcp: Adapter type: QDIO\n' + 'zhcp: Adapter status: Not coupled\n' + 'zhcp: LAN owner: \n' + 'zhcp: LAN name: '] + smapi_err = ['Failed'] + xcat_req = mock.Mock() + xcat_req.side_effect = [{'data': [nic_err]}, + {'data': [smapi_err]}] + with mock.patch('neutron.plugins.zvm.common.xcatutils.xcat_request', + xcat_req): + with mock.patch.object(utils, "LOG") as log: + self._utils.create_xcat_mgt_network(self._FAKE_ZHCP_NODENAME, + "10.1.1.1", + "255.255.0.0", + self._FAKE_VSWITCH) + self._verify_query_nic(nic_err, xcat_req) + log.error.assert_called_with('NIC 800 staus is unknown.') + + self.assertRaises(exception.zvmException, + self._utils.create_xcat_mgt_network, + self._FAKE_ZHCP_NODENAME, + "10.1.1.1", + "255.255.0.0", + self._FAKE_VSWITCH) + self._verify_query_nic(smapi_err, xcat_req) + + def test_re_grant_user(self): + '''We assume there is three nodes valid in the xCAT MN db, they are: + node1, node2, node4. We mock _MAX_REGRANT_USER_NUMBER to 2. So the + process of regrant has two steps. Fisrt grant two nodes and then + grant one node.''' + + fake_port_info = ['node1,switch,port1,10,inf1,fakezhcp,false', + 'node2,switch,port2,10,inf2,fakezhcp,false', + # node3's zhcp field is invalid + 'node3,switch,port3,10,inf3,zhcp,false', + 'node4,switch,port3,10,inf4,fakezhcp,false'] + self._utils.get_nic_ids = mock.MagicMock(return_value=fake_port_info) + + fake_user_id = ['#node,hcp,userid,nodetype,parent,comments,disable', + '"opnstk1","zhcp.ibm.com",,,,,', # invalid record + '"node1","fakezhcp","user01",,,,', + '"zhcp2","zhcp2.ibm.com","ZHCP",,,,', # invalid record + '"node2","fakezhcp","user02",,,,', + '"node3","zhcp","user03",,,,', # invalid record + '"node4","fakezhcp","user04",,,,'] + + xcat_req = mock.Mock() + xcat_req.side_effect = [{'data': [fake_user_id]}, + {'data': [['OK']]}, # run_command step 1, regrant two node + {'data': [['OK']]}, # run_command remove + {'data': [['OK']]}, # run_command step 2 + {'data': [['OK']]}] # run_command remove + + with mock.patch.object(utils.zvmUtils, '_MAX_REGRANT_USER_NUMBER', 2): + with mock.patch( + 'neutron.plugins.zvm.common.xcatutils.xcat_request', + xcat_req): + self._utils.re_grant_user(self._FAKE_ZHCP_NODENAME) + url_command = ('/xcatws/nodes/fakezhcp/dsh?userName=' + 'fake_xcat_user&password=fake_xcat_password&format=json') + + valid_users = [1, 2, 4] + last_user = None + + # re_grant_user uses a dict to keep the ports info, so we don't + # know the order, which nodes are reganted in step 1 and which + # one is regranted in step 2. We will try to find the node + # removed in step 2 first, because this is easier. Then we + # verify the step 1. + for i in valid_users: + cmd_vsw_couple =\ + ('command=echo -e "#!/bin/sh\n/opt/zhcp/bin/smcli' + ' Virtual_Network_Vswitch_Set_Extended -T user0%s -k' + ' switch_name=switch -k grant_userid=user0%s' + ' -k user_vlan_id=10" > grant.sh' % (i, i)) + if mock.call('PUT', url_command, [cmd_vsw_couple]) in\ + xcat_req.call_args_list: + last_user = i + break + self.assertTrue(last_user) + # remove the node from valid users, so we can verify if the + # other two nodes has been regranted via the valid_users. + del(valid_users[valid_users.index(last_user)]) + + body_cmd_node_1 =\ + ('command=echo -e "#!/bin/sh\n' + '/opt/zhcp/bin/smcli Virtual_Network_Vswitch_Set_Extended' + ' -T user0%s -k switch_name=switch -k grant_userid=user0%s' + ' -k user_vlan_id=10\n' + % (valid_users[0], valid_users[0])) +\ + ('/opt/zhcp/bin/smcli Virtual_Network_Vswitch_Set_Extended' + ' -T user0%s -k switch_name=switch -k grant_userid=user0%s' + ' -k user_vlan_id=10" > grant.sh' + % (valid_users[1], valid_users[1])) + + body_cmd_node_2 =\ + ('command=echo -e "#!/bin/sh\n' + '/opt/zhcp/bin/smcli Virtual_Network_Vswitch_Set_Extended' + ' -T user0%s -k switch_name=switch -k grant_userid=user0%s' + ' -k user_vlan_id=10\n' + % (valid_users[1], valid_users[1])) +\ + ('/opt/zhcp/bin/smcli Virtual_Network_Vswitch_Set_Extended' + ' -T user0%s -k switch_name=switch -k grant_userid=user0%s' + ' -k user_vlan_id=10" > grant.sh' + % (valid_users[0], valid_users[0])) + self.assertTrue( + (mock.call('PUT', url_command, [body_cmd_node_1]) + in xcat_req.call_args_list) + or (mock.call('PUT', url_command, [body_cmd_node_2]) + in xcat_req.call_args_list)) + + def test_query_xcat_uptime(self): + xcat_uptime = {'data': + [['XCAT was activated on 2014-06-11 at 02:41:15']]} + xcat_req = mock.Mock(return_value=xcat_uptime) + with mock.patch('neutron.plugins.zvm.common.xcatutils.xcat_request', + xcat_req): + with mock.patch.object(utils.zvmUtils, "get_userid_from_node", + mock.Mock(return_value='xcat')): + ret = self._utils.query_xcat_uptime(self._FAKE_ZHCP_NODENAME) + self.assertEqual(ret, '2014-06-11 at 02:41:15') + url = ('/xcatws/nodes/fakezhcp/dsh?userName=fake_xcat_user&' + 'password=fake_xcat_password&format=json') + body = ['command=/opt/zhcp/bin/smcli' + ' Image_Query_Activate_Time -T xcat -f 4'] + xcat_req.assert_called_with('PUT', url, body) + + def test_query_zvm_uptime(self): + fake_ret = ('timezone\ncurrent time\nversion\nGen time\n' + 'zhcp: The z/VM CP IPL time: 2014-06-11 01:38:37 EDT\n' + 'storage\n') + zvm_uptime = {'data': [[fake_ret]]} + xcat_req = mock.Mock(return_value=zvm_uptime) + with mock.patch('neutron.plugins.zvm.common.xcatutils.xcat_request', + xcat_req): + ret = self._utils.query_zvm_uptime(self._FAKE_ZHCP_NODENAME) + self.assertEqual(ret, '2014-06-11 01:38:37 EDT') + url = ('/xcatws/nodes/fakezhcp/dsh?userName=fake_xcat_user&' + 'password=fake_xcat_password&format=json') + body = ['command=/opt/zhcp/bin/smcli System_Info_Query'] + xcat_req.assert_called_with('PUT', url, body) diff --git a/neutron-zvm-plugin/neutron/tests/unit/zvm/test_zvm_xcatutils.py b/neutron-zvm-plugin/neutron/tests/unit/zvm/test_zvm_xcatutils.py new file mode 100755 index 0000000..b6dc631 --- /dev/null +++ b/neutron-zvm-plugin/neutron/tests/unit/zvm/test_zvm_xcatutils.py @@ -0,0 +1,41 @@ +# vim: tabstop=4 shiftwidth=4 softtabstop=4 + +# Copyright 2014 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. + +""" +Unit tests for the z/VM xCAT utils. +""" + +import mock + +from oslo.config import cfg +from neutron.plugins.zvm.common import xcatutils +from neutron.tests import base + + +class TestZVMXcatUtils(base.BaseTestCase): + _FAKE_XCAT_SERVER = "127.0.0.1" + _FAKE_XCAT_TIMEOUT = 300 + + def setUp(self): + super(TestZVMXcatUtils, self).setUp() + cfg.CONF.set_override('zvm_xcat_server', + self._FAKE_XCAT_SERVER, 'AGENT') + cfg.CONF.set_override('zvm_xcat_timeout', + self._FAKE_XCAT_TIMEOUT, 'AGENT') + self._xcaturl = xcatutils.xCatURL() + with mock.patch.multiple(xcatutils.httplib, + HTTPSConnection=mock.MagicMock()): + self._zvm_xcat_connection = xcatutils.xCatConnection() diff --git a/nova-zvm-virt-driver/nova/tests/test_zvm.py b/nova-zvm-virt-driver/nova/tests/test_zvm.py new file mode 100755 index 0000000..d317034 --- /dev/null +++ b/nova-zvm-virt-driver/nova/tests/test_zvm.py @@ -0,0 +1,2936 @@ +# vim: tabstop=4 shiftwidth=4 softtabstop=4 + +# Copyright 2013 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. + +"""Test suite for ZVMDriver.""" + +import __builtin__ +import httplib +import mock +import mox +import os +import socket + +from nova.compute import power_state +from nova import context +from nova import db +from nova import exception as nova_exception +from nova.image import glance +from nova.openstack.common.gettextutils import _ +from nova.openstack.common import fileutils +from nova.openstack.common import jsonutils +from nova import test +from nova.virt import fake +from nova.virt.zvm import configdrive +from nova.virt.zvm import driver +from nova.virt.zvm import exception +from nova.virt.zvm import imageop +from nova.virt.zvm import instance +from nova.virt.zvm import networkop +from nova.virt.zvm import utils as zvmutils +from nova.virt.zvm import volumeop +from oslo.config import cfg + + +CONF = cfg.CONF + + +class FakeXCATConn(object): + + def __init__(self): + pass + + def request(self, one, two, three=None, four={}): + pass + + +class FakeHTTPResponse(object): + + def __init__(self, status=None, reason=None, data=None): + self.status = status + self.reason = reason + self.data = data + + def read(self): + return self.data + + +class FakeImageService(object): + + def __init__(self, values): + self.values = values + + def show(self, *args): + return self.values + + def delete(self, *args): + pass + + def update(self, *args): + pass + + +class FakeInstMeta(object): + + def metadata_for_config_drive(self): + return [('openstack', 'data1'), ('ec2', 'data2')] + + +class ZVMTestCase(test.TestCase): + """Base testcase class of zvm driver and zvm instance.""" + + def setUp(self): + super(ZVMTestCase, self).setUp() + self.context = context.get_admin_context() + self.instance = db.instance_create(self.context, + {'user_id': 'fake', + 'project_id': 'fake', + 'instance_type_id': 1, + 'memory_mb': 1024, + 'vcpus': 2, + 'root_gb': 10, + 'ephemeral_gb': 0, + 'image_ref': '0000-1111', + 'host': 'fakenode'}) + self.instance2 = db.instance_create(self.context, + {'user_id': 'fake', + 'project_id': 'fake', + 'instance_type_id': 1, + 'memory_mb': 1024, + 'vcpus': 2, + 'root_gb': 10, + 'ephemeral_gb': 1, + 'image_ref': '0000-1111', + 'host': 'fakenode'}) + self.flags(host='fakehost', + my_ip='10.1.1.10', + zvm_xcat_server='10.10.10.10', + zvm_xcat_username='fake', + zvm_xcat_password='fake', + zvm_host='fakenode', + zvm_diskpool='fakedp', + zvm_diskpool_type='FBA', + instance_name_template='os%06x', + zvm_xcat_master='fakemn', + zvm_scsi_pool='fakesp', + network_api_class='nova.network.neutronv2.api.API', + zvm_image_default_password='pass', + zvm_fcp_list="1FB0-1FB3", + zvm_zhcp_fcp_list="1FAF", + config_drive_format='tgz', + zvm_image_compression_level='0') + + def tearDown(self): + self.addCleanup(self.stubs.UnsetAll) + self.mox.UnsetStubs() + super(ZVMTestCase, self).tearDown() + + def _set_fake_xcat_responses(self, fake_resp_list): + self.stubs.Set(zvmutils, "XCATConnection", FakeXCATConn) + self.mox.StubOutWithMock(zvmutils.XCATConnection, 'request') + for res_data in fake_resp_list: + res = {'message': jsonutils.dumps(res_data)} + zvmutils.XCATConnection.request(mox.IgnoreArg(), mox.IgnoreArg(), + mox.IgnoreArg(), mox.IgnoreArg()).AndReturn(res) + self.mox.ReplayAll() + + def _set_fake_xcat_resp(self, fake_resp_list): + """fake_resq_list: + [(method, url, body, response_data), + (method, url, body, response_data), + ..., + (method, url, body, response_data)] + """ + self.mox.StubOutWithMock(zvmutils.XCATConnection, 'request') + for res in fake_resp_list: + method = res[0] or mox.IgnoreArg() + url = res[1] or mox.IgnoreArg() + body = res[2] or mox.IgnoreArg() + res = {'message': jsonutils.dumps(res[3])} + zvmutils.XCATConnection.request(method, url, body, + mox.IgnoreArg()).AndReturn(res) + self.mox.ReplayAll() + + def _gen_resp(self, **kwargs): + data = [] + for (k, v) in kwargs.iteritems(): + if v not in (None, []): + data.append({k: v}) + + return {'data': data} + + def _generate_xcat_resp(self, info): + return {'data': [{'info': info}]} + + def _fake_fun(self, value=None): + return lambda *args, **kwargs: value + + def _app_auth(self, url): + return ''.join([url, '?userName=fake&password=fake&format=json']) + + +class ZVMDriverTestCases(ZVMTestCase): + """Unit tests for z/VM driver methods.""" + + fake_image_meta = { + 'checksum': '1a2bbbdbcc9c536a2688fc6278685dfb', + 'container_format': 'bare', + 'disk_format': 'raw', + 'id': 'bef39792-1ae2-46f5-b44c-0641bfcb3b98', + 'is_public': False, + 'name': 'fakeimg', + 'properties': {'architecture': 's390x', + 'image_file_name': 'abc', + 'image_type': 'linux', + 'os_name': 'Linux', + 'os_version': 'rhel6.2', + 'provisioning_method': 'netboot'}, + 'size': 578181045, + 'status': 'active'} + + keys = ('image_ref', 'uuid', 'user_id', 'project_id', + 'power_state', 'system_metadata', 'memory_mb', 'vcpus', + 'root_gb', 'ephemeral_gb') + _fake_inst = {'name': 'os000001'} + _old_inst = {'name': 'rszos000001'} + for k in keys: + _fake_inst[k] = '' + _old_inst[k] = '' + + def _fake_host_rinv_info(self): + fake_host_rinv_info = ["fakenode: z/VM Host: FAKENODE\n" + "fakenode: zHCP: fakehcp.fake.com\n" + "fakenode: CEC Vendor: FAKE\n" + "fakenode: CEC Model: 2097\n" + "fakenode: Hypervisor OS: z/VM 6.1.0\n" + "fakenode: Hypervisor Name: fakenode\n" + "fakenode: Architecture: s390x\n" + "fakenode: LPAR CPU Total: 10\n" + "fakenode: LPAR CPU Used: 10\n" + "fakenode: LPAR Memory Total: 16G\n" + "fakenode: LPAR Memory Offline: 0\n" + "fakenode: LPAR Memory Used: 16.0G\n" + "fakenode: IPL Time:" + "IPL at 03/13/14 21:43:12 EDT\n"] + return self._generate_xcat_resp(fake_host_rinv_info) + + def _fake_disk_info(self): + fake_disk_info = ["fakenode: FAKEDP Total: 406105.3 G\n" + "fakenode: FAKEDP Used: 367262.6 G\n" + "fakenode: FAKEDP Free: 38842.7 G\n"] + return self._generate_xcat_resp(fake_disk_info) + + def _set_fake_host_info(self): + self._set_fake_xcat_responses([self._fake_host_rinv_info(), + self._fake_disk_info(), self._gen_resp(info=['userid=fakehcp'])]) + + def _setup_fake_inst_obj(self): + keys = ('image_ref', 'uuid', 'user_id', 'project_id', + 'power_state', 'system_metadata', 'memory_mb', 'vcpus', + 'root_gb', 'ephemeral_gb') + self._fake_inst = {'name': 'os000001'} + self._old_inst = {'name': 'rszos000001'} + for k in keys: + self._fake_inst[k] = '' + self._old_inst[k] = '' + + def setUp(self): + super(ZVMDriverTestCases, self).setUp() + self._set_fake_host_info() + self._setup_fake_inst_obj() + self.driver = driver.ZVMDriver(fake.FakeVirtAPI()) + self.mox.UnsetStubs() + + def test_init_driver(self): + self.assertTrue(isinstance(self.driver._xcat_url, zvmutils.XCATUrl)) + self.assertTrue(isinstance(self.driver._zvm_images, imageop.ZVMImages)) + self.assertTrue(isinstance(self.driver._pathutils, zvmutils.PathUtils)) + self.assertTrue(isinstance(self.driver._networkop, + networkop.NetworkOperator)) + + def test_update_host_info(self): + self._set_fake_xcat_responses([self._fake_host_rinv_info(), + self._fake_disk_info()]) + host_info = self.driver.update_host_status()[0] + self.mox.VerifyAll() + self.assertEqual(host_info['hypervisor_hostname'], 'fakenode') + self.assertEqual(host_info['host_memory_total'], 16 * 1024) + + def _fake_instance_list_data(self): + return {'data': [{'data': self._fake_instances_list()}]} + + def _fake_instances_list(self): + inst_list = ['#node,hcp,userid,nodetype,parent,comments,disable', + '"fakehcp","fakehcp.fake.com","HCP","vm","fakenode"', + '"fakenode","fakehcp.fake.com",,,,,', + '"os000001","fakehcp.fake.com","OS000001",,,,'] + return inst_list + + def test_list_instances(self): + self._set_fake_xcat_responses([self._fake_instance_list_data()]) + inst_list = self.driver.list_instances() + self.mox.VerifyAll() + self.assertTrue("os000001" in inst_list) + + def test_list_instances_exclude_xcat_master(self): + self.flags(zvm_xcat_master='xcat') + fake_inst_list = self._fake_instances_list() + fake_inst_list.append('"xcat","fakexcat.fake.com",,,,,') + self._set_fake_xcat_responses([ + {'data': [{'data': fake_inst_list}]}]) + inst_list = self.driver.list_instances() + self.mox.VerifyAll() + self.assertTrue("os000001" in inst_list) + self.assertTrue("xcat" not in inst_list) + + def test_get_available_resource(self): + self._set_fake_xcat_responses([self._fake_host_rinv_info(), + self._fake_disk_info()]) + res = self.driver.get_available_resource('fakenode') + self.mox.VerifyAll() + self.assertEqual(res['vcpus'], 10) + self.assertEqual(res['memory_mb_used'], 16 * 1024) + self.assertEqual(res['disk_available_least'], 38843) + + def _fake_instance_info(self): + inst_inv_info = [ + "os000001: Uptime: 4 days 20 hr 00 min\n" + "os000001: CPU Used Time: 330528353\n" + "os000001: Total Memory: 128M\n" + "os000001: Max Memory: 2G\n" + "os000001: \n" + "os000001: Processors: \n" + "os000001: CPU 03 ID FF00EBBE20978000 CP CPUAFF ON\n" + "os000001: CPU 00 ID FF00EBBE20978000 (BASE) CP CPUAFF ON\n" + "os000001: CPU 01 ID FF00EBBE20978000 CP CPUAFF ON\n" + "os000001: CPU 02 ID FF00EBBE20978000 CP CPUAFF ON\n" + "os000001: \n" + ] + return self._generate_xcat_resp(inst_inv_info) + + def _fake_reachable_data(self, stat): + return {"data": [{"node": [{"name": ["os000001"], "data": [stat]}]}]} + + def test_get_info(self): + power_stat = self._generate_xcat_resp(['os000001: on\n']) + self._set_fake_xcat_responses([power_stat, + self._fake_reachable_data('sshd'), + self._fake_instance_info()]) + inst_info = self.driver.get_info(self.instance) + self.mox.VerifyAll() + self.assertEqual(0x01, inst_info['state']) + self.assertEqual(131072, inst_info['mem']) + self.assertEqual(4, inst_info['num_cpu']) + self.assertEqual(330528353, inst_info['cpu_time']) + self.assertEqual(1048576, inst_info['max_mem']) + + def test_get_info_not_exist(self): + def _fake_get_info(*args): + msg = 'Forbidden: Invalid nodes and/or groups' + raise exception.ZVMXCATRequestFailed(xcatserver='fakemn', + msg=msg) + self.stubs.Set(instance.ZVMInstance, 'get_info', _fake_get_info) + self.assertRaises(nova_exception.InstanceNotFound, + self.driver.get_info, self.instance2) + + def test_get_info_from_down(self): + power_stat = self._generate_xcat_resp(['os000001: off\n']) + self._set_fake_xcat_responses([power_stat, + self._fake_reachable_data('noping')]) + inst_info = self.driver.get_info(self.instance) + self.mox.VerifyAll() + self.assertEqual(0x04, inst_info['state']) + self.assertEqual(0, inst_info['mem']) + self.assertEqual(0, inst_info['cpu_time']) + + def test_get_info_from_paused(self): + power_stat = self._generate_xcat_resp(['os000001: on\n']) + self._set_fake_xcat_responses([power_stat, + self._fake_reachable_data('noping')]) + self.instance['power_state'] = power_state.PAUSED + inst_info = self.driver.get_info(self.instance) + self.mox.VerifyAll() + self.assertEqual(0x03, inst_info['state']) + self.assertEqual(1048576, inst_info['mem']) + self.assertEqual(2, inst_info['num_cpu']) + + def test_destroy(self): + rmvm_info = ["os000001: Deleting virtual server OS000001... Done"] + fake_resp_list = [ + ("GET", None, None, self._fake_instance_list_data()), + ("DELETE", None, None, self._gen_resp(info=rmvm_info))] + self._set_fake_xcat_resp(fake_resp_list) + self.driver.destroy({}, self.instance, {}, {}) + self.mox.VerifyAll() + + def test_destroy_failed(self): + rmvm_info = ["os000001: Deleting virtual server OS000001... Failed"] + det_res = self._gen_resp(info=rmvm_info, error=['error']) + fake_resp_list = [ + ("GET", None, None, self._fake_instance_list_data()), + ("DELETE", None, None, det_res)] + self._set_fake_xcat_resp(fake_resp_list) + self.assertRaises(exception.ZVMXCATInternalError, + self.driver.destroy, {}, self.instance, {}, {}) + self.mox.VerifyAll() + + def test_destroy_non_exist(self): + self._set_fake_xcat_responses([self._fake_instance_list_data()]) + self.driver.destroy({}, self.instance2, {}, {}) + self.mox.VerifyAll() + + def _fake_image_meta(self): + return {'checksum': '1a2bbbdbcc9c536a2688fc6278685dfb', + 'container_format': 'bare', + 'disk_format': 'raw', + 'id': 'bef39792-1ae2-46f5-b44c-0641bfcb3b98', + 'is_public': False, + 'name': 'fakeimg', + 'properties': {'architecture': 's390x', + 'image_file_name': 'abc', + 'image_type_xcat': 'linux', + 'os_name': 'Linux', + 'os_version': 'rhel6.2', + 'provisioning_method': 'netboot', + 'root_disk_units': 578181045, + }, + 'size': 578181045, + 'status': 'active', + 'min_disk': 3, + } + + def _fake_network_info(self): + info = \ + [ + ( + {'ovs_interfaceid': '6ef5433c-f29b-4bcc-b8c5-f5159d7e05ba', + 'network': ( + {'bridge': 'br-int', + 'subnets': [({ + 'ips': [({'meta': {}, + 'version': 4, + 'type': 'fixed', + 'floating_ips': [], + 'address': '10.1.11.51' + })], + 'version': 4, + 'meta': {}, + 'dns': [], + 'routes': [], + 'cidr': '10.1.0.0/16', + 'gateway': ({'meta': {}, + 'version': 4, + 'type': 'gateway', + 'address': u'10.1.0.1' + }) + })], + 'meta': {'injected': False, + 'tenant_id': '0be9e98fdf6d4f599632226154c6c86c'}, + 'id': '01921c21-0373-4ccf-934e-9c6b2ccd7bdc', + 'label': u'xcat_management' + } + ), + 'devname': 'tap6ef5433c-f2', + 'qbh_params': None, + 'meta': {}, + 'address': '02:00:00:ee:ae:51', + 'type': 'ovs', + 'id': '6ef5433c-f29b-4bcc-b8c5-f5159d7e05ba', + 'qbg_params': None + } + ) + ] + return info + + def test_spawn(self): + self.instance['config_drive'] = True + self.stubs.Set(self.driver._pathutils, 'get_instance_path', + self._fake_fun('/temp/os000001')) + self.stubs.Set(self.driver, '_create_config_drive', + self._fake_fun('/temp/os000001/configdrive.tgz')) + self.stubs.Set(self.driver._networkutils, + "create_network_configuration_files", + self._fake_fun(('/tmp/fakefile', 'fakecmd'))) + self.stubs.Set(instance.ZVMInstance, 'create_xcat_node', + self._fake_fun()) + self.stubs.Set(self.driver, '_preset_instance_network', + self._fake_fun()) + self.stubs.Set(self.driver._zvm_images, 'image_exist_xcat', + self._fake_fun(False)) + self.stubs.Set(self.driver, '_import_image_to_xcat', self._fake_fun()) + self.stubs.Set(instance.ZVMInstance, 'create_userid', self._fake_fun()) + self.stubs.Set(instance.ZVMInstance, 'update_node_info', + self._fake_fun()) + self.stubs.Set(self.driver._zvm_images, 'get_imgname_xcat', + self._fake_fun('fakeimg')) + self.stubs.Set(self.driver._networkutils, + "create_network_configuration_files", + self._fake_fun((['fakefile', 'fakecmd']))) + self.stubs.Set(instance.ZVMInstance, 'deploy_node', self._fake_fun()) + self.stubs.Set(self.driver._pathutils, 'clean_temp_folder', + self._fake_fun()) + self.stubs.Set(self.driver._networkop, 'create_nic', self._fake_fun()) + self.stubs.Set(zvmutils, 'punch_adminpass_file', self._fake_fun()) + self.stubs.Set(zvmutils, 'punch_xcat_auth_file', self._fake_fun()) + self.stubs.Set(instance.ZVMInstance, 'power_on', self._fake_fun()) + self.stubs.Set(self.driver._zvm_images, 'update_last_use_date', + self._fake_fun()) + self.stubs.Set(self.driver, '_wait_for_addnic', self._fake_fun()) + self.stubs.Set(self.driver, '_is_nic_granted', self._fake_fun(True)) + self.driver.spawn({}, self.instance, self._fake_image_meta(), ['fake'], + 'fakepass', self._fake_network_info(), {}) + + def test_spawn_with_eph(self): + self.instance['config_drive'] = True + self.stubs.Set(self.driver._pathutils, 'get_instance_path', + self._fake_fun('/temp/os000001')) + self.stubs.Set(self.driver, '_create_config_drive', + self._fake_fun('/temp/os000001/configdrive.tgz')) + self.stubs.Set(self.driver._networkutils, + "create_network_configuration_files", + self._fake_fun(('/tmp/fakefile', 'fakecmd'))) + self.stubs.Set(instance.ZVMInstance, 'create_xcat_node', + self._fake_fun()) + self.stubs.Set(self.driver, '_preset_instance_network', + self._fake_fun()) + self.stubs.Set(self.driver._zvm_images, 'image_exist_xcat', + self._fake_fun(False)) + self.stubs.Set(self.driver, '_import_image_to_xcat', self._fake_fun()) + self.stubs.Set(instance.ZVMInstance, 'create_userid', self._fake_fun()) + self.stubs.Set(instance.ZVMInstance, 'update_node_info', + self._fake_fun()) + self.stubs.Set(self.driver._zvm_images, 'get_imgname_xcat', + self._fake_fun('fakeimg')) + self.stubs.Set(self.driver._networkutils, + "create_network_configuration_files", + self._fake_fun((['fakefile', 'fakecmd']))) + self.stubs.Set(instance.ZVMInstance, 'deploy_node', self._fake_fun()) + self.stubs.Set(self.driver._pathutils, 'clean_temp_folder', + self._fake_fun()) + self.stubs.Set(self.driver._networkop, 'create_nic', self._fake_fun()) + self.stubs.Set(zvmutils, 'punch_adminpass_file', self._fake_fun()) + self.stubs.Set(zvmutils, 'punch_xcat_auth_file', self._fake_fun()) + self.stubs.Set(instance.ZVMInstance, 'power_on', self._fake_fun()) + self.stubs.Set(self.driver._zvm_images, 'update_last_use_date', + self._fake_fun()) + self.stubs.Set(self.driver, '_wait_for_addnic', self._fake_fun()) + self.stubs.Set(self.driver, '_is_nic_granted', self._fake_fun(True)) + self.stubs.Set(__builtin__, 'open', mock.mock_open()) + self.stubs.Set(os, 'remove', self._fake_fun()) + self.stubs.Set(zvmutils, 'get_host', self._fake_fun("fake@10.1.1.10")) + punch_body = ['--punchfile /temp/os000001/eph.disk X fake@10.1.1.10'] + self._set_fake_xcat_resp([ + ("PUT", None, punch_body, self._gen_resp(info='done')), + ]) + self.driver.spawn({}, self.instance2, self._fake_image_meta(), + ['fake'], 'fakepass', self._fake_network_info(), {}) + self.mox.VerifyAll() + + def test_spawn_with_eph_opts(self): + self.instance['config_drive'] = True + self.instance['ephemeral_gb'] = 2 + network_info = self._fake_network_info() + image_meta = self._fake_image_meta() + fake_bdi = {'ephemerals': [ + {'device_name': '/dev/sdb', + 'device_type': None, + 'disk_bus': None, + 'guest_format': u'ext4', + 'size': 2}, + {'device_name': '/dev/sdc', + 'device_type': None, + 'disk_bus': None, + 'guest_format': u'ext3', + 'size': 1}]} + + self.mox.StubOutWithMock(self.driver._pathutils, 'get_instance_path') + self.mox.StubOutWithMock(self.driver._networkutils, + "create_network_configuration_files") + self.mox.StubOutWithMock(self.driver, '_create_config_drive') + self.mox.StubOutWithMock(instance.ZVMInstance, 'create_xcat_node') + self.mox.StubOutWithMock(self.driver, '_preset_instance_network') + self.mox.StubOutWithMock(self.driver._zvm_images, 'image_exist_xcat') + self.mox.StubOutWithMock(instance.ZVMInstance, 'create_userid') + self.mox.StubOutWithMock(instance.ZVMInstance, 'update_node_info') + self.mox.StubOutWithMock(self.driver._networkop, 'create_nic') + self.mox.StubOutWithMock(self.driver._zvm_images, 'get_imgname_xcat') + self.mox.StubOutWithMock(instance.ZVMInstance, 'deploy_node') + self.mox.StubOutWithMock(self.driver._pathutils, 'clean_temp_folder') + self.mox.StubOutWithMock(zvmutils, 'punch_adminpass_file') + self.mox.StubOutWithMock(zvmutils, 'punch_xcat_auth_file') + self.mox.StubOutWithMock(zvmutils, 'punch_eph_info_file') + self.mox.StubOutWithMock(self.driver, '_wait_for_addnic') + self.mox.StubOutWithMock(self.driver, '_is_nic_granted') + self.mox.StubOutWithMock(instance.ZVMInstance, 'power_on') + self.mox.StubOutWithMock(self.driver._zvm_images, + 'update_last_use_date') + + self.driver._pathutils.get_instance_path('fakenode', + 'os000001').AndReturn('/temp/os000001') + self.driver._networkutils.create_network_configuration_files( + mox.IgnoreArg(), network_info, mox.IgnoreArg(), + mox.IgnoreArg()).AndReturn(('/tmp/fakefile', 'fakecmd')) + self.driver._create_config_drive('/temp/os000001', self.instance, + mox.IgnoreArg(), mox.IgnoreArg(), mox.IgnoreArg()).AndReturn( + '/temp/os000001/configdrive.tgz') + instance.ZVMInstance.create_xcat_node('fakehcp.fake.com') + self.driver._preset_instance_network('os000001', network_info) + self.driver._zvm_images.image_exist_xcat(mox.IgnoreArg()).AndReturn( + True) + instance.ZVMInstance.create_userid(fake_bdi, + image_meta).AndReturn(None) + instance.ZVMInstance.update_node_info(image_meta) + self.driver._networkop.create_nic(mox.IgnoreArg(), 'os000001', + mox.IgnoreArg(), mox.IgnoreArg(), '1000') + self.driver._zvm_images.get_imgname_xcat(mox.IgnoreArg()).AndReturn( + 'fakeimg') + instance.ZVMInstance.deploy_node('fakeimg', + '/temp/os000001/configdrive.tgz') + zvmutils.punch_adminpass_file(mox.IgnoreArg(), 'os000001', 'pass') + zvmutils.punch_xcat_auth_file(mox.IgnoreArg(), 'os000001') + zvmutils.punch_eph_info_file(mox.IgnoreArg(), 'os000001', + mox.IgnoreArg(), mox.IgnoreArg(), mox.IgnoreArg()) + zvmutils.punch_eph_info_file(mox.IgnoreArg(), 'os000001', + mox.IgnoreArg(), mox.IgnoreArg(), mox.IgnoreArg()) + self.driver._wait_for_addnic('os000001').AndReturn(True) + self.driver._is_nic_granted('os000001').AndReturn(True) + instance.ZVMInstance.power_on() + self.driver._pathutils.clean_temp_folder(mox.IgnoreArg()) + self.driver._zvm_images.update_last_use_date(mox.IgnoreArg()) + self.mox.ReplayAll() + + self.driver.spawn(self.context, self.instance, self._fake_image_meta(), + ['fake'], 'fakepass', self._fake_network_info(), fake_bdi) + self.mox.VerifyAll() + + def test_spawn_image_error(self): + self.stubs.Set(self.driver._pathutils, 'get_instance_path', + self._fake_fun('/temp/os000001')) + self.stubs.Set(self.driver, '_create_config_drive', + self._fake_fun('/temp/os000001/configdrive.tgz')) + self.stubs.Set(instance.ZVMInstance, 'create_xcat_node', + self._fake_fun()) + self.stubs.Set(self.driver, '_preset_instance_network', + self._fake_fun()) + self.stubs.Set(self.driver._networkutils, + "create_network_configuration_files", + self._fake_fun(('/tmp/fakefile', 'fakecmd'))) + self.mox.StubOutWithMock(self.driver._zvm_images, 'image_exist_xcat') + self.driver._zvm_images.image_exist_xcat(mox.IgnoreArg()).\ + AndRaise(exception.ZVMImageError(msg='fake')) + self.mox.ReplayAll() + self.stubs.Set(instance.ZVMInstance, 'delete_xcat_node', + self._fake_fun()) + self.assertRaises(exception.ZVMImageError, self.driver.spawn, {}, + self.instance, self._fake_image_meta(), [], + 'fakepass', self._fake_network_info(), {}) + self.mox.VerifyAll() + + def test_spawn_deploy_failed(self): + self.stubs.Set(self.driver._pathutils, 'get_instance_path', + self._fake_fun('/temp/os000001')) + self.stubs.Set(instance.ZVMInstance, 'create_xcat_node', + self._fake_fun()) + self.stubs.Set(self.driver, '_preset_instance_network', + self._fake_fun()) + self.stubs.Set(self.driver._zvm_images, 'image_exist_xcat', + self._fake_fun(False)) + self.stubs.Set(self.driver._networkutils, + "create_network_configuration_files", + self._fake_fun(("/tmp/fakefile", "fakecmd"))) + self.stubs.Set(self.driver, '_import_image_to_xcat', self._fake_fun()) + self.stubs.Set(instance.ZVMInstance, 'create_userid', self._fake_fun()) + self.stubs.Set(self.driver._networkop, 'create_nic', self._fake_fun()) + self.stubs.Set(instance.ZVMInstance, 'update_node_info', + self._fake_fun()) + self.stubs.Set(self.driver._zvm_images, 'get_imgname_xcat', + self._fake_fun('fakeimg')) + self.mox.StubOutWithMock(instance.ZVMInstance, 'deploy_node') + instance.ZVMInstance.deploy_node(mox.IgnoreArg(), mox.IgnoreArg()).\ + AndRaise(exception.ZVMXCATDeployNodeFailed(node='os000001', + msg='fake')) + self.mox.ReplayAll() + self.stubs.Set(self.driver._pathutils, 'clean_temp_folder', + self._fake_fun()) + self.stubs.Set(self.driver, 'destroy', self._fake_fun()) + self.assertRaises(exception.ZVMXCATDeployNodeFailed, self.driver.spawn, + {}, self.instance, self._fake_image_meta(), [], + 'fakepass', self._fake_network_info(), {}) + self.mox.VerifyAll() + + def _set_reachable(self, stat): + return {"data": [{"node": [{"name": ["os000001"], + "data": [stat]}]}]} + + def test_power_on(self): + info = ["os000001: Activating OS000001... Done\n"] + self._set_fake_xcat_responses([self._generate_xcat_resp(info), + self._set_reachable('sshd')]) + self.driver.power_on({}, self.instance, {}) + + def _fake_manifest(self): + return {'imagename': 'fakeimg', + 'imagetype': 'raw', + 'osarch': 's390x', + 'osname': 'fakeos', + 'osvers': 'fakev', + 'profile': 'fakeprof', + 'provmethod': 'netboot'} + + def test_snapshot(self): + if not os.path.exists("/tmp/fakeimg"): + os.mknod("/tmp/fakeimg") + self.instance['power_state'] = power_state.RUNNING + self.stubs.Set(glance, 'get_remote_image_service', + self._fake_fun((FakeImageService(self.fake_image_meta), 0))) + url_fspace = ''.join([self._app_auth("/xcatws/nodes/fakemn/inventory"), + "&field=--freerepospace"]) + res_fspace = self._gen_resp(info=["gpok164: Free Image " + "Repository: 13.9G"]) + url_xdsh = self._app_auth('/xcatws/nodes/os000001/dsh') + body_cmd = ["command=df -h /"] + res_img_need = self._gen_resp(data=["Filesystem Size Used Avail Use% " + "Mounted on /dev/dasda1 6.8G " + "5.2G 1.3G 81% /"]) + self._set_fake_xcat_resp([ + ("GET", url_fspace, None, res_fspace), + ('PUT', url_xdsh, body_cmd, res_img_need), + ]) + self.stubs.Set(self.driver._zvm_images, 'create_zvm_image', + self._fake_fun('')) + self.stubs.Set(self.driver, 'power_on', self._fake_fun()) + self.stubs.Set(self.driver._zvm_images, 'get_image_from_xcat', + self._fake_fun(('/tmp', 'fakeimg.tgz'))) + self.stubs.Set(self.driver._zvm_images, 'delete_image_from_xcat', + self._fake_fun()) + self.stubs.Set(self.driver._zvm_images, 'update_last_use_date', + self._fake_fun()) + self.stubs.Set(self.driver._zvm_images, 'untar_image_bundle', + self._fake_fun()) + self.stubs.Set(self.driver._zvm_images, 'parse_manifest_xml', + self._fake_fun(self._fake_manifest())) + self.stubs.Set(self.driver._zvm_images, 'get_image_file_name', + self._fake_fun('fakeimg')) + self.stubs.Set(self.driver._zvm_images, 'clean_up_snapshot_time_path', + self._fake_fun()) + self.stubs.Set(self.driver._zvm_images, 'get_root_disk_units', + self._fake_fun(1111)) + self.stubs.Set(os, 'makedirs', self._fake_fun()) + self.stubs.Set(__builtin__, 'open', mock.mock_open()) + self.driver.snapshot({}, self.instance, '0000-1111', self._fake_fun()) + self.mox.VerifyAll() + + def test_snapshot_capture_failed(self): + self.instance['power_state'] = power_state.SHUTDOWN + self.stubs.Set(self.driver, 'power_on', self._fake_fun()) + self.stubs.Set(glance, 'get_remote_image_service', + self._fake_fun((FakeImageService(self._fake_image_meta()), 0))) + self.stubs.Set(self.driver._zvm_images, 'get_free_space_xcat', + self._fake_fun(20.0)) + self.stubs.Set(self.driver._zvm_images, 'get_imgcapture_needed', + self._fake_fun(10.0)) + self.mox.StubOutWithMock(self.driver._zvm_images, 'create_zvm_image') + self.driver._zvm_images.create_zvm_image(mox.IgnoreArg(), + mox.IgnoreArg(), mox.IgnoreArg()).\ + AndRaise(exception.ZVMImageError(msg='fake')) + self.mox.ReplayAll() + self.stubs.Set(self.driver._zvm_images, 'delete_image_glance', + self._fake_fun()) + self.assertRaises(exception.ZVMImageError, self.driver.snapshot, + {}, self.instance, '0000-1111', self._fake_fun()) + self.mox.VerifyAll() + + def test_reboot_hard(self): + info1 = ["os000001: Shutting down... Done"] + info2 = ["os000001: Activating... Done"] + data = {"data": [{"info": info1}, {"info": info2}]} + self._set_fake_xcat_responses([data, self._set_reachable('sshd')]) + self.driver.reboot(self.context, self.instance, {}, "HARD") + self.mox.VerifyAll() + + def test_reboot_hard_from_down(self): + info1 = ["os000001: Shutting down... Failed"] + info2 = ["os000001: Activating... Done"] + data = {"data": [{"info": info1}, {"info": info2}]} + self._set_fake_xcat_responses([data, self._set_reachable('sshd')]) + self.driver.reboot(self.context, self.instance, {}, "HARD") + self.mox.VerifyAll() + + def test_reboot_hard_failed(self): + info1 = ["os000001: Shutting down... Failed"] + info2 = ["os000001: Activating... Failed"] + self._set_fake_xcat_responses([self._gen_resp(info=[info1, info2], + error=['(error)'])]) + self.assertRaises(exception.ZVMXCATInternalError, + self.driver.reboot, + self.context, self.instance, {}, "HARD") + + def test_reboot_soft(self): + info1 = ["os000001: Shutting down... Done"] + info2 = ["os000001: Activating... Done"] + data = {"data": [{"info": info1}, {"info": info2}]} + self._set_fake_xcat_responses([data, self._set_reachable('sshd')]) + self.driver.reboot(self.context, self.instance, {}, "SOFT") + self.mox.VerifyAll() + + def test_reboot_soft_failed(self): + info1 = ["os000001: Shutting down... Failed\n"] + info2 = ["os000001: Activating... Failed\n"] + self._set_fake_xcat_responses([self._gen_resp(info=[info1, info2], + error=['(error)'])]) + self.assertRaises(exception.ZVMXCATInternalError, + self.driver.reboot, + self.context, self.instance, {}, "SOFT") + + def test_power_off(self): + info = ["os000001: Stopping OS000001... Done\n"] + self._set_fake_xcat_responses([self._generate_xcat_resp(info)]) + self.driver.power_off(self.instance, 10, 10) + self.mox.VerifyAll() + + def test_power_off_inactive(self): + info = ["os000001: Stopping LINUX171... Failed\n" + " Return Code: 200\n" + " Reason Code: 12\n" + " Description: Image not active\n"] + self._set_fake_xcat_responses([self._generate_xcat_resp(info)]) + self.driver.power_off(self.instance) + self.mox.VerifyAll() + + def test_power_off_failed(self): + info = ["os000001: Stopping OS000001... Failed\n"] + self._set_fake_xcat_responses([self._gen_resp(info=info, + error=['error'])]) + self.assertRaises(nova_exception.InstancePowerOffFailure, + self.driver.power_off, self.instance) + + def _fake_connection_info(self): + conn_info = { + 'driver_volume_type': 'fibre_channel', + 'data': {'volume_name': 'fakev', + 'volume_id': 1, + 'volume_type': '', + 'volume_status': 'online', + 'volume_size': 10, + 'provider_location': '500507630B40C038:401340C900000000', + 'zvm_fcp': 'F000', + 'target_lun': '442', + 'target_wwn': '401340C900000000', + 'host': 'fakehost', + } + } + return conn_info + + def test_attach_volume(self): + self.mox.StubOutWithMock(self.driver, '_format_mountpoint') + self.mox.StubOutWithMock(self.driver, 'instance_exists') + self.mox.StubOutWithMock(instance.ZVMInstance, 'is_reachable') + self.mox.StubOutWithMock(instance.ZVMInstance, 'attach_volume') + self.driver._format_mountpoint('/dev/sdd').AndReturn('/dev/vdd') + self.driver.instance_exists('os000001').AndReturn(True) + instance.ZVMInstance.is_reachable().AndReturn(True) + instance.ZVMInstance.attach_volume(self.driver._volumeop, {}, + self._fake_connection_info(), + self.instance, '/dev/vdd', True) + self.mox.ReplayAll() + + self.driver.attach_volume({}, self._fake_connection_info(), + self.instance, '/dev/sdd') + self.mox.VerifyAll() + + def test_attach_volume_no_mountpoint(self): + self.mox.StubOutWithMock(self.driver, 'instance_exists') + self.mox.StubOutWithMock(instance.ZVMInstance, 'is_reachable') + self.mox.StubOutWithMock(instance.ZVMInstance, 'attach_volume') + self.driver.instance_exists('os000001').AndReturn(True) + instance.ZVMInstance.is_reachable().AndReturn(True) + instance.ZVMInstance.attach_volume(self.driver._volumeop, {}, + self._fake_connection_info(), + self.instance, None, True) + self.mox.ReplayAll() + + self.driver.attach_volume({}, self._fake_connection_info(), + self.instance, None) + self.mox.VerifyAll() + + def test_live_migration_same_mn(self): + dest = 'fhost2' + migrate_data = {'dest_host': dest, + 'pre_live_migration_result': + {'same_xcat_mn': True, + 'dest_diff_mn_key': None}} + info = ["os000001: VMRELOCATE action=move against OS000001... Done\n"] + self._set_fake_xcat_responses([self._generate_xcat_resp(info)]) + self.driver.live_migration(self.context, self.instance, dest, + self._fake_fun(), self._fake_fun(), None, + migrate_data) + self.mox.VerifyAll() + + def test_live_migration_diff_mn(self): + dest = 'fhost2' + migrate_data = {'dest_host': dest, + 'pre_live_migration_result': + {'same_xcat_mn': False, + 'dest_diff_mn_key': 'sshkey'}} + info = ["os000001: VMRELOCATE action=move against OS000001... Done\n"] + self.stubs.Set(zvmutils, "xdsh", self._fake_fun()) + self.stubs.Set(self.driver._networkop, 'clean_mac_switch_host', + self._fake_fun()) + self.stubs.Set(instance.ZVMInstance, 'delete_xcat_node', + self._fake_fun()) + self._set_fake_xcat_resp([("PUT", None, None, + self._gen_resp(info=info))]) + self.driver.live_migration(self.context, self.instance, dest, + self._fake_fun(), self._fake_fun(), None, + migrate_data) + self.mox.VerifyAll() + + def test_live_migration_failed(self): + dest = 'fhost2' + migrate_data = {'dest_host': dest, + 'pre_live_migration_result': + {'same_xcat_mn': True, + 'dest_diff_mn_key': None}} + info = ["os000001: VMRELOCATE MOVE error\n"] + self._set_fake_xcat_responses([self._gen_resp(info=info, + error=['error'])]) + self.stubs.Set(zvmutils, "xdsh", self._fake_fun()) + self.assertRaises(nova_exception.MigrationError, + self.driver.live_migration, self.context, self.instance, dest, + self._fake_fun(), self._fake_fun(), None, migrate_data) + self.mox.VerifyAll() + + def test_check_can_live_migrate_destination(self): + dest = 'fhost2' + dest_info = {'hypervisor_hostname': dest} + res_dict = self.driver.check_can_live_migrate_destination(self.context, + self.instance, {}, dest_info, {}, {}) + exp_dict = {'dest_host': dest, + 'is_shared_storage': True} + self.assertEqual(res_dict.get('migrate_data', None), exp_dict) + + def test_check_can_live_migrate_source(self): + dest = 'fhost2' + migrate_data = {'dest_host': dest, + 'is_shared_storage': True} + dest_check_data = {'migrate_data': migrate_data} + info = ["os000001: VMRELOCATE action=test against OS000001... Done\n"] + self._set_fake_xcat_resp([ + ("GET", None, None, self._gen_resp(info=["userid=os000001"])), + ("PUT", None, None, self._gen_resp(info=info))]) + res_dict = self.driver.check_can_live_migrate_source( + self.context, self.instance, dest_check_data) + self.assertNotEqual(res_dict.get('dest_host', None), None) + self.mox.ReplayAll() + + def test_check_can_live_migrate_source_failed(self): + dest = 'fhost2' + migrate_data = {'dest_host': dest} + dest_check_data = {'migrate_data': migrate_data} + error = ["VMRELOCATE move failed"] + + self.mox.StubOutWithMock(zvmutils, "get_userid") + self.mox.StubOutWithMock(self.driver, "_vmrelocate") + zvmutils.get_userid('os000001').AndReturn("os000001") + self.driver._vmrelocate('fhost2', 'os000001', 'test').AndRaise( + nova_exception.MigrationError(reason=error)) + self.mox.ReplayAll() + + self.assertRaises(nova_exception.MigrationError, + self.driver.check_can_live_migrate_source, self.context, + self.instance, dest_check_data) + self.mox.VerifyAll() + + def test_check_can_live_migrate_source_force(self): + self.flags(zvm_vmrelocate_force='domain') + dest = 'fhost2' + migrate_data = {'dest_host': dest} + dest_check_data = {'migrate_data': migrate_data} + + self.mox.StubOutWithMock(zvmutils, "get_userid") + self.mox.StubOutWithMock(self.driver, "_vmrelocate") + zvmutils.get_userid('os000001').AndReturn("os000001") + self.driver._vmrelocate('fhost2', 'os000001', 'test').AndRaise( + nova_exception.MigrationError(reason="1944")) + self.mox.ReplayAll() + + self.driver.check_can_live_migrate_source(self.context, self.instance, + dest_check_data) + self.mox.VerifyAll() + + def test_migrate_disk_and_power_off(self): + fake_sys_meta = {'new_instance_type_root_gb': 20, + 'new_instance_type_ephemeral_gb': 20, + 'instance_type_root_gb': 10, + 'instance_type_ephemeral_gb': 10} + inst = {'name': 'os000001', + 'power_state': 1, + 'system_metadata': fake_sys_meta, + 'uuid': '1111-1111-1111-1111'} + self.stubs.Set(zvmutils, 'get_host', self._fake_fun("root@10.1.1.10")) + self.mox.StubOutWithMock(zvmutils, 'get_userid') + self.mox.StubOutWithMock(self.driver, '_get_eph_disk_info') + self.mox.StubOutWithMock(self.driver, '_detach_volume_from_instance') + self.mox.StubOutWithMock(self.driver, '_capture_disk_for_instance') + + zvmutils.get_userid('os000001').AndReturn('os000001') + self.driver._get_eph_disk_info('os000001').AndReturn([]) + self.driver._detach_volume_from_instance(mox.IgnoreArg(), + mox.IgnoreArg()) + self.driver._capture_disk_for_instance({}, mox.IgnoreArg()).AndReturn( + ('/tmp/bundle', 'fakeimagename')) + self.mox.ReplayAll() + + disk_info = self.driver.migrate_disk_and_power_off({}, inst, + '10.1.1.11', None, [({}, {})]) + self.mox.VerifyAll() + + exp = ('{"eph_disk_info": [], "disk_source_image": ' + '"root@10.1.1.10:/tmp/bundle", "disk_source_mn": "10.10.10.10",' + ' "disk_owner": "os000001", "disk_eph_size_old": 10, ' + '"disk_type": "FBA", "disk_image_name": "fakeimagename", ' + '"disk_eph_size_new": 20}') + self.assertEqual(exp, disk_info) + + def test_migrate_disk_and_power_off_not_support(self): + fake_sys_meta = {'new_instance_type_root_gb': 10, + 'new_instance_type_ephemeral_gb': 10, + 'instance_type_root_gb': 20, + 'instance_type_ephemeral_gb': 20} + inst = {'name': 'os000001', + 'power_state': 1, + 'system_metadata': fake_sys_meta, + 'uuid': '1111-1111-1111-1111'} + self._set_fake_xcat_resp([ + ("GET", None, None, self._gen_resp(info=["userid=os000001"]))]) + self.assertRaises(nova_exception.MigrationError, + self.driver.migrate_disk_and_power_off, self.context, + inst, '10.1.1.11', None, [({}, {})]) + + def test__get_eph_disk_info(self): + self.mox.StubOutWithMock(self.driver, '_get_user_directory') + self.driver._get_user_directory('os000001').AndReturn([ + 'os000001: MDISK 0100 3390 n 100 n MR\n', + 'os000001: MDISK 0102 3390 n 200 n MR\n', + 'os000001: MDISK 0103 3390 n 300 n MR\n']) + self.mox.ReplayAll() + + eph_disk_info = self.driver._get_eph_disk_info('os000001') + exp = [{'device_name': '0102', + 'guest_format': None, + 'size': '200', + 'size_in_units': True, + 'vdev': '0102'}, + {'device_name': '0103', + 'guest_format': None, + 'size': '300', + 'size_in_units': True, + 'vdev': '0103'}] + self.assertEqual(exp, eph_disk_info) + + def test__get_eph_disk_info_invalid_resp(self): + self.mox.StubOutWithMock(self.driver, '_get_user_directory') + self.driver._get_user_directory('os000001').AndReturn([ + 'os000001: MDISK 0101\n']) + self.mox.ReplayAll() + + self.assertRaises(exception.ZVMInvalidXCATResponseDataError, + self.driver._get_eph_disk_info, 'os000001') + + def test_finish_migration_same_mn(self): + self.flags(zvm_xcat_server="10.10.10.10") + network_info = self._fake_network_info() + disk_info = { + 'disk_type': 'FBA', + 'disk_source_mn': '10.10.10.10', + 'disk_source_image': 'root@10.1.1.10:/fakepath/fa-ke-ima-ge.tgz', + 'disk_image_name': 'fa-ke-ima-ge', + 'disk_owner': 'os000001', + 'disk_eph_size_old': 0, + 'disk_eph_size_new': 0, + 'eph_disk_info': []} + migration = {'source_node': 'FAKENODE1', + 'dest_node': 'FAKENODE2'} + disk_info = jsonutils.dumps(disk_info) + + self.mox.StubOutWithMock(self.driver, 'get_host_ip_addr') + self.mox.StubOutWithMock(self.driver._networkop, + 'clean_mac_switch_host') + self.mox.StubOutWithMock(self.driver._pathutils, 'clean_temp_folder') + self.mox.StubOutWithMock(self.driver, '_copy_instance') + self.mox.StubOutWithMock(instance.ZVMInstance, 'copy_xcat_node') + self.mox.StubOutWithMock(instance.ZVMInstance, 'update_node_def') + self.mox.StubOutWithMock(self.driver, '_preset_instance_network') + self.mox.StubOutWithMock(instance.ZVMInstance, 'create_userid') + self.mox.StubOutWithMock(self.driver, '_add_nic_to_instance') + self.mox.StubOutWithMock(self.driver, '_deploy_root_and_ephemeral') + self.mox.StubOutWithMock(self.driver._zvm_images, + 'delete_image_from_xcat') + self.mox.StubOutWithMock(zvmutils, 'punch_xcat_auth_file') + self.mox.StubOutWithMock(instance.ZVMInstance, 'power_on') + self.mox.StubOutWithMock(zvmutils, 'xdsh') + self.mox.StubOutWithMock(self.driver, '_attach_volume_to_instance') + + farg = mox.IgnoreArg() + self.driver.get_host_ip_addr().AndReturn('10.1.1.10') + self.driver._networkop.clean_mac_switch_host(farg) + self.driver._pathutils.clean_temp_folder(farg) + self.driver._copy_instance(farg).AndReturn(self._fake_inst) + instance.ZVMInstance.copy_xcat_node(farg) + instance.ZVMInstance.update_node_def(farg, farg) + self.driver._preset_instance_network('os000001', farg) + instance.ZVMInstance.create_userid(farg, farg) + self.driver._add_nic_to_instance('os000001', farg, farg) + self.driver._deploy_root_and_ephemeral(farg, farg) + self.driver._zvm_images.delete_image_from_xcat(farg) + zvmutils.punch_xcat_auth_file(mox.IgnoreArg(), 'os000001') + instance.ZVMInstance.power_on() + self.driver._attach_volume_to_instance(farg, self._fake_inst, []) + self.mox.ReplayAll() + + self.driver.finish_migration(self.context, migration, self._fake_inst, + disk_info, network_info, None, None) + self.mox.VerifyAll() + + def test_finish_migration_same_mn_with_eph(self): + self.flags(zvm_xcat_server="10.10.10.10") + network_info = self._fake_network_info() + disk_info = { + 'disk_type': 'FBA', + 'disk_source_mn': '10.10.10.10', + 'disk_source_image': 'root@10.1.1.10:/fakepath/fa-ke-ima-ge.tgz', + 'disk_image_name': 'fa-ke-ima-ge', + 'disk_owner': 'os000001', + 'disk_eph_size_old': 0, + 'disk_eph_size_new': 1, + 'eph_disk_info': []} + migration = {'source_node': 'FAKENODE1', + 'dest_node': 'FAKENODE2'} + disk_info = jsonutils.dumps(disk_info) + + self.mox.StubOutWithMock(self.driver, 'get_host_ip_addr') + self.mox.StubOutWithMock(self.driver._networkop, + 'clean_mac_switch_host') + self.mox.StubOutWithMock(self.driver._pathutils, 'clean_temp_folder') + self.mox.StubOutWithMock(self.driver, '_copy_instance') + self.mox.StubOutWithMock(instance.ZVMInstance, 'copy_xcat_node') + self.mox.StubOutWithMock(instance.ZVMInstance, 'update_node_def') + self.mox.StubOutWithMock(self.driver, '_preset_instance_network') + self.mox.StubOutWithMock(instance.ZVMInstance, 'create_userid') + self.mox.StubOutWithMock(self.driver._pathutils, 'get_instance_path') + self.mox.StubOutWithMock(zvmutils, 'punch_eph_info_file') + self.mox.StubOutWithMock(self.driver, '_add_nic_to_instance') + self.mox.StubOutWithMock(self.driver, '_deploy_root_and_ephemeral') + self.mox.StubOutWithMock(self.driver._zvm_images, + 'delete_image_from_xcat') + self.mox.StubOutWithMock(zvmutils, 'punch_xcat_auth_file') + self.mox.StubOutWithMock(instance.ZVMInstance, 'power_on') + self.mox.StubOutWithMock(self.driver, '_attach_volume_to_instance') + + farg = mox.IgnoreArg() + self.driver.get_host_ip_addr().AndReturn('10.1.1.10') + self.driver._networkop.clean_mac_switch_host(farg) + self.driver._pathutils.clean_temp_folder(farg) + self.driver._copy_instance(farg).AndReturn(self._fake_inst) + instance.ZVMInstance.copy_xcat_node(farg) + instance.ZVMInstance.update_node_def(farg, farg) + self.driver._preset_instance_network('os000001', farg) + instance.ZVMInstance.create_userid(farg, farg) + self.driver._pathutils.get_instance_path(farg, farg).AndReturn('/fp') + zvmutils.punch_eph_info_file('/fp', 'os000001') + self.driver._add_nic_to_instance('os000001', farg, farg) + self.driver._deploy_root_and_ephemeral(farg, farg) + self.driver._zvm_images.delete_image_from_xcat(farg) + zvmutils.punch_xcat_auth_file(mox.IgnoreArg(), 'os000001') + instance.ZVMInstance.power_on() + self.driver._attach_volume_to_instance(farg, self._fake_inst, []) + self.mox.ReplayAll() + + self.driver.finish_migration(self.context, migration, self._fake_inst, + disk_info, network_info, None, None) + self.mox.VerifyAll() + + def test_finish_migration_same_mn_deploy_failed(self): + """Exception raised while deploying, verify networking re-configured""" + self.flags(zvm_xcat_server="10.10.10.10") + network_info = self._fake_network_info() + disk_info = { + 'disk_type': 'FBA', + 'disk_source_mn': '10.10.10.10', + 'disk_source_image': 'root@10.1.1.10:/fakepath/fa-ke-ima-ge.tgz', + 'disk_image_name': 'fa-ke-ima-ge', + 'disk_owner': 'os000001', + 'disk_eph_size_old': 0, + 'disk_eph_size_new': 0, + 'eph_disk_info': []} + migration = {'source_node': 'FAKENODE1', + 'dest_node': 'FAKENODE2'} + disk_info = jsonutils.dumps(disk_info) + + self.mox.StubOutWithMock(self.driver, 'get_host_ip_addr') + self.mox.StubOutWithMock(self.driver._networkop, + 'clean_mac_switch_host') + self.mox.StubOutWithMock(self.driver._pathutils, 'clean_temp_folder') + self.mox.StubOutWithMock(self.driver, '_copy_instance') + self.mox.StubOutWithMock(instance.ZVMInstance, 'copy_xcat_node') + self.mox.StubOutWithMock(instance.ZVMInstance, 'update_node_def') + self.mox.StubOutWithMock(self.driver, '_preset_instance_network') + self.mox.StubOutWithMock(instance.ZVMInstance, 'create_userid') + self.mox.StubOutWithMock(self.driver, '_add_nic_to_instance') + self.mox.StubOutWithMock(self.driver, '_deploy_root_and_ephemeral') + self.mox.StubOutWithMock(self.driver._zvm_images, + 'delete_image_from_xcat') + self.mox.StubOutWithMock(instance.ZVMInstance, 'delete_userid') + self.mox.StubOutWithMock(instance.ZVMInstance, 'delete_xcat_node') + self.mox.StubOutWithMock(self.driver, '_reconfigure_networking') + self.mox.StubOutWithMock(self.driver, '_is_nic_granted') + self.mox.StubOutWithMock(instance.ZVMInstance, 'power_on') + + farg = mox.IgnoreArg() + self.driver.get_host_ip_addr().AndReturn('10.1.1.10') + self.driver._networkop.clean_mac_switch_host(farg) + self.driver._pathutils.clean_temp_folder(farg) + self.driver._copy_instance(farg).AndReturn(self._fake_inst) + instance.ZVMInstance.copy_xcat_node(farg) + instance.ZVMInstance.update_node_def(farg, farg) + self.driver._preset_instance_network('os000001', farg) + instance.ZVMInstance.create_userid(farg, farg) + self.driver._add_nic_to_instance('os000001', farg, farg) + self.driver._deploy_root_and_ephemeral(farg, farg).AndRaise( + exception.ZVMXCATDeployNodeFailed({'node': 'fn', 'msg': 'e'})) + self.driver._zvm_images.delete_image_from_xcat(farg) + instance.ZVMInstance.delete_userid('fakehcp') + instance.ZVMInstance.delete_xcat_node() + instance.ZVMInstance.copy_xcat_node(farg) + instance.ZVMInstance.delete_xcat_node() + self.driver._reconfigure_networking(farg, network_info, farg) + self.driver._is_nic_granted(farg).AndReturn(True) + instance.ZVMInstance.power_on() + self.mox.ReplayAll() + + self.assertRaises(exception.ZVMXCATDeployNodeFailed, + self.driver.finish_migration, self.context, migration, + self._fake_inst, disk_info, network_info, None, None) + self.mox.VerifyAll() + + def test_finish_migration_diff_mn(self): + self.flags(zvm_xcat_server="10.10.10.11") + network_info = self._fake_network_info() + disk_info = { + 'disk_type': 'FBA', + 'disk_source_mn': '10.10.10.10', + 'disk_source_image': 'root@10.1.1.10:/fakepath/fa-ke-ima-ge.tgz', + 'disk_image_name': 'fa-ke-ima-ge', + 'disk_owner': 'os000001', + 'disk_eph_size_old': 0, + 'disk_eph_size_new': 0, + 'eph_disk_info': []} + migration = {'source_node': 'FAKENODE1', + 'dest_node': 'FAKENODE2'} + disk_info = jsonutils.dumps(disk_info) + + self.mox.StubOutWithMock(self.driver, 'get_host_ip_addr') + self.mox.StubOutWithMock(instance.ZVMInstance, 'create_xcat_node') + self.mox.StubOutWithMock(instance.ZVMInstance, 'update_node_def') + self.mox.StubOutWithMock(self.driver._zvm_images, 'put_image_to_xcat') + self.mox.StubOutWithMock(self.driver._zvm_images, + 'clean_up_snapshot_time_path') + self.mox.StubOutWithMock(self.driver, '_preset_instance_network') + self.mox.StubOutWithMock(instance.ZVMInstance, 'create_userid') + self.mox.StubOutWithMock(self.driver, '_add_nic_to_instance') + self.mox.StubOutWithMock(self.driver, '_deploy_root_and_ephemeral') + self.mox.StubOutWithMock(self.driver._zvm_images, + 'delete_image_from_xcat') + self.mox.StubOutWithMock(zvmutils, 'punch_xcat_auth_file') + self.mox.StubOutWithMock(instance.ZVMInstance, 'power_on') + self.mox.StubOutWithMock(self.driver, '_attach_volume_to_instance') + + farg = mox.IgnoreArg() + self.driver.get_host_ip_addr().AndReturn('10.1.1.10') + instance.ZVMInstance.create_xcat_node(farg) + instance.ZVMInstance.update_node_def(farg, farg) + self.driver._zvm_images.put_image_to_xcat(farg, farg) + self.driver._zvm_images.clean_up_snapshot_time_path(farg) + self.driver._preset_instance_network('os000001', farg) + instance.ZVMInstance.create_userid(farg, farg) + self.driver._add_nic_to_instance('os000001', farg, farg) + self.driver._deploy_root_and_ephemeral(farg, farg) + self.driver._zvm_images.delete_image_from_xcat(farg) + zvmutils.punch_xcat_auth_file(farg, farg) + instance.ZVMInstance.power_on() + self.driver._attach_volume_to_instance(farg, self._fake_inst, []) + self.mox.ReplayAll() + + self.driver.finish_migration(self.context, migration, self._fake_inst, + disk_info, network_info, None, None) + self.mox.VerifyAll() + + def test_confirm_migration_same_mn(self): + self.flags(zvm_xcat_server="10.10.10.10") + self.stubs.Set(self.driver, 'instance_exists', self._fake_fun(True)) + self.mox.StubOutWithMock(self.driver, 'destroy') + self.driver.destroy({}, self._old_inst) + self.mox.ReplayAll() + self.driver.confirm_migration([], self._fake_inst, []) + self.mox.VerifyAll() + + def test_confirm_migration_diff_mn(self): + self.flags(zvm_xcat_server="10.10.10.11") + self.stubs.Set(self.driver, 'instance_exists', self._fake_fun(False)) + self.stubs.Set(self.driver._zvm_images, 'delete_image_from_xcat', + self._fake_fun()) + self.stubs.Set(self.driver._zvm_images, + 'cleanup_image_after_migration', self._fake_fun()) + self.mox.StubOutWithMock(self.driver, 'destroy') + self.driver.destroy({}, self._fake_inst) + self.mox.ReplayAll() + self.driver.confirm_migration([], self._fake_inst, []) + self.mox.VerifyAll() + + def test_finish_revert_migration_same_mn(self): + self.flags(zvm_xcat_server="10.10.10.10") + self.stubs.Set(instance.ZVMInstance, 'copy_xcat_node', + self._fake_fun()) + self.stubs.Set(instance.ZVMInstance, 'delete_xcat_node', + self._fake_fun()) + + self.mox.StubOutWithMock(self.driver, 'instance_exists') + self.mox.StubOutWithMock(self.driver, '_preset_instance_network') + self.mox.StubOutWithMock(self.driver, '_add_nic_to_instance') + self.mox.StubOutWithMock(self.driver, '_wait_for_nic_update') + self.mox.StubOutWithMock(self.driver, '_wait_for_addnic') + self.mox.StubOutWithMock(self.driver, '_is_nic_granted') + self.mox.StubOutWithMock(self.driver, '_attach_volume_to_instance') + self.mox.StubOutWithMock(self.driver, 'power_on') + + self.driver.instance_exists('rszos000001').AndReturn(True) + self.driver._preset_instance_network('os000001', + self._fake_network_info()).AndReturn(None) + self.driver._add_nic_to_instance('os000001', + self._fake_network_info(), None).AndReturn(None) + self.driver._wait_for_nic_update('os000001').AndReturn(None) + self.driver._wait_for_addnic('os000001').AndReturn(None) + self.driver._is_nic_granted('os000001').AndReturn(True) + self.driver._attach_volume_to_instance({}, mox.IgnoreArg(), + []).AndReturn(None) + self.driver.power_on({}, mox.IgnoreArg(), []).AndReturn(None) + self.mox.ReplayAll() + + self.driver.finish_revert_migration({}, self._fake_inst, + self._fake_network_info(), None, True) + self.mox.VerifyAll() + + def test_finish_revert_migration_diff_mn(self): + self.flags(zvm_xcat_server="10.10.10.11") + + self.mox.StubOutWithMock(self.driver, 'instance_exists') + self.mox.StubOutWithMock(self.driver._zvm_images, + 'cleanup_image_after_migration') + self.mox.StubOutWithMock(self.driver, '_attach_volume_to_instance') + + self.driver.instance_exists('rszos000001').AndReturn(False) + self.driver._zvm_images.cleanup_image_after_migration( + 'os000001').AndReturn(None) + self.driver._attach_volume_to_instance({}, mox.IgnoreArg(), + []).AndReturn(None) + self.mox.ReplayAll() + + self.driver.finish_revert_migration({}, self._fake_inst, + self._fake_network_info(), None, False) + +# def test_get_host_uptime(self): +# ipl_time = 'IPL at 03/13/14 21:43:12 EDT' +# self.assertEqual(self.driver.get_host_uptime(None), ipl_time) + + def _fake_console_rinv_info(self): + fake_console_rinv_info = ["fakenode: 00: zIPL boot menu\n"] + return self._generate_xcat_resp(fake_console_rinv_info) + + def test_get_console_output(self): + self._set_fake_xcat_responses([self._fake_console_rinv_info()]) + console_log = self.driver.get_console_output(self.context, + self.instance).split('\n')[0] + self.mox.VerifyAll() + self.assertEqual('fakenode: 00: zIPL boot menu', console_log) + + def test_create_config_drive_non_tgz(self): + self.flags(config_drive_format='nontgz') + self.assertRaises(exception.ZVMConfigDriveError, + self.driver._create_config_drive, '', self.instance, '', '', '') + + def test_get_hcp_info(self): + hcp_info = self.driver._get_hcp_info() + self.assertEqual('fakehcp.fake.com', hcp_info['hostname']) + self.assertEqual('fakehcp', hcp_info['nodename']) + self.assertEqual('fakehcp', hcp_info['userid']) + + def test_get_hcp_info_first_call(self): + self.driver._host_stats = [] + + self.mox.StubOutWithMock(zvmutils, 'get_userid') + zvmutils.get_userid('fakehcp').AndReturn('fakehcp') + self.mox.ReplayAll() + + hcp_info = self.driver._get_hcp_info('fakehcp.fake.com') + self.mox.VerifyAll() + self.assertEqual('fakehcp.fake.com', hcp_info['hostname']) + self.assertEqual('fakehcp', hcp_info['nodename']) + self.assertEqual('fakehcp', hcp_info['userid']) + + def test_get_hcp_info_incorrect_init(self): + self.driver._host_stats = [] + fake_hcpinfo = {'hostname': 'fakehcp.fake.com', + 'nodename': 'fakehcp', + 'userid': 'fakehcp'} + + self.mox.StubOutWithMock(self.driver, 'update_host_status') + self.driver.update_host_status().AndReturn([{'zhcp': fake_hcpinfo}]) + self.mox.ReplayAll() + + hcp_info = self.driver._get_hcp_info() + self.mox.VerifyAll() + self.assertEqual('fakehcp.fake.com', hcp_info['hostname']) + self.assertEqual('fakehcp', hcp_info['nodename']) + self.assertEqual('fakehcp', hcp_info['userid']) + + def test_detach_volume_from_instance(self): + bdm = [{'connection_info': 'fake', 'mount_device': None}] + self.mox.StubOutWithMock(self.driver, 'instance_exists') + self.mox.StubOutWithMock(instance.ZVMInstance, 'is_reachable') + self.mox.StubOutWithMock(instance.ZVMInstance, 'detach_volume') + self.driver.instance_exists('os000001').AndReturn(True) + instance.ZVMInstance.is_reachable().AndReturn(True) + instance.ZVMInstance.detach_volume(self.driver._volumeop, 'fake', + mox.IgnoreArg(), None, True, rollback=False) + self.mox.ReplayAll() + + self.driver._detach_volume_from_instance(self.instance, bdm) + self.mox.VerifyAll() + + +class ZVMInstanceTestCases(ZVMTestCase): + """Test cases for zvm.instance.""" + + def setUp(self): + super(ZVMInstanceTestCases, self).setUp() + self.flags(zvm_user_profile='fakeprof') + self.instance['ephemeral_gb'] = 0 + self._instance = instance.ZVMInstance(self.instance) + + def test_create_xcat_node(self): + info = ["1 object definitions have been created or modified."] + self._set_fake_xcat_responses([self._generate_xcat_resp(info)]) + self._instance.create_xcat_node('fakehcp') + self.mox.VerifyAll() + + def test_create_xcat_node_failed(self): + resp = {'data': [{'errorcode': ['1'], + 'error': ["One or more errors occured\n"]}]} + self._set_fake_xcat_responses([resp]) + self.assertRaises(exception.ZVMXCATCreateNodeFailed, + self._instance.create_xcat_node, 'fakehcp') + self.mox.VerifyAll() + + def test_add_mdisk_eckd(self): + info = ["os000001: Adding a disk to LINUX171... Done\n" + "os000001: Active disk configuration... Done\n"] + self._set_fake_xcat_responses([self._generate_xcat_resp(info)]) + self._instance.add_mdisk('fakedp', '0101', '1g') + + def test_add_mdisk_fba_with_fmt(self): + self.flags(zvm_diskpool_type='FBA') + info = ["os000001: Adding a disk to LINUX171... Done\n" + "os000001: Active disk configuration... Done\n"] + punch_body = ["--add9336 fakedp 0101 1g MR '' '' '' ext3"] + self._set_fake_xcat_resp([ + ("PUT", None, punch_body, self._gen_resp(info=info)) + ]) + self._instance.add_mdisk('fakedp', '0101', '1g', 'ext3') + self.mox.VerifyAll() + + def test_set_ipl(self): + info = ["os000001: Adding IPL statement to OS000001's " + "directory entry... Done\n"] + self._set_fake_xcat_responses([self._generate_xcat_resp(info)]) + self._instance._set_ipl('0100') + + def test_create_userid(self): + """Create userid.""" + info = ['os000001: Defining OS000001 in directory... Done\n' + 'os000001: Granting VSwitch for OS000001... Done\n'] + self._set_fake_xcat_responses([self._generate_xcat_resp(info)]) + self.stubs.Set(self._instance, '_set_ipl', lambda *args: None) + self.stubs.Set(self._instance, 'add_mdisk', lambda *args: None) + self._instance.create_userid({}, {'min_disk': 3}) + + def test_create_userid_has_ephemeral(self): + """Create userid with epheral disk added.""" + self._instance._instance['ephemeral_gb'] = 20 + cu_info = ['os000001: Defining OS000001 in directory... Done\n' + 'os000001: Granting VSwitch for OS000001... Done\n'] + am_info = ["os000001: Adding a disk to OS00001... Done\n" + "os000001: Active disk configuration... Done\n"] + self._set_fake_xcat_responses([self._generate_xcat_resp(cu_info), + self._generate_xcat_resp(am_info), + self._generate_xcat_resp(am_info)]) + self.stubs.Set(self._instance, '_set_ipl', lambda *args: None) + self._instance.create_userid({}, {'min_disk': 3}) + self.mox.VerifyAll() + + def test_create_userid_with_eph_opts(self): + """Create userid with '--ephemeral' options.""" + self._instance._instance['ephemeral_gb'] = 20 + fake_bdi = {'ephemerals': [ + {'device_name': '/dev/sdb', + 'device_type': None, + 'disk_bus': None, + 'guest_format': u'ext4', + 'size': 2}, + {'device_name': '/dev/sdc', + 'device_type': None, + 'disk_bus': None, + 'guest_format': u'ext3', + 'size': 1}]} + + self.mox.StubOutWithMock(zvmutils, 'xcat_request') + self.mox.StubOutWithMock(self._instance, 'add_mdisk') + self.mox.StubOutWithMock(self._instance, '_set_ipl') + + zvmutils.xcat_request('POST', mox.IgnoreArg(), mox.IgnoreArg()) + self._instance.add_mdisk('fakedp', '0100', '10g') + self._instance._set_ipl('0100') + self._instance.add_mdisk('fakedp', '0102', '2g', 'ext4') + self._instance.add_mdisk('fakedp', '0103', '1g', 'ext3') + self.mox.ReplayAll() + + self._instance.create_userid(fake_bdi, {}) + self.mox.VerifyAll() + + def test_create_userid_with_eph_opts_resize(self): + """Create userid with '--ephemeral' options.""" + self._instance._instance['ephemeral_gb'] = 20 + fake_bdi = {'ephemerals': [ + {'device_name': '/dev/sdb', + 'device_type': None, + 'disk_bus': None, + 'guest_format': u'ext4', + 'size': '200000', + 'size_in_units': True}, + {'device_name': '/dev/sdc', + 'device_type': None, + 'disk_bus': None, + 'guest_format': u'ext3', + 'size': '100000', + 'size_in_units': True}]} + + self.mox.StubOutWithMock(zvmutils, 'xcat_request') + self.mox.StubOutWithMock(self._instance, 'add_mdisk') + self.mox.StubOutWithMock(self._instance, '_set_ipl') + + zvmutils.xcat_request('POST', mox.IgnoreArg(), mox.IgnoreArg()) + self._instance.add_mdisk('fakedp', '0100', '10g') + self._instance._set_ipl('0100') + self._instance.add_mdisk('fakedp', '0102', '200000', 'ext4') + self._instance.add_mdisk('fakedp', '0103', '100000', 'ext3') + self.mox.ReplayAll() + + self._instance.create_userid(fake_bdi, {}) + self.mox.VerifyAll() + + def test_update_node_info(self): + image_meta = {'name': 'fake', + 'id': '00-11-22-33', + 'properties': {'os_version': 'fake', + 'architecture': 'fake', + 'provisioning_method': 'fake'}} + info = ['os000001: update node info ... Done\n'] + self._set_fake_xcat_responses([self._generate_xcat_resp(info)]) + self._instance.update_node_info(image_meta) + + def test_deploy_node(self): + self._set_fake_xcat_responses([self._generate_xcat_resp(['fake'])]) + self._instance.deploy_node('fakeimg', '/fake/file', '0100') + + def test_deploy_node_failed(self): + self._set_fake_xcat_responses([{'data': [{'error': ['fake']}]}]) + self.assertRaises(exception.ZVMXCATDeployNodeFailed, + self._instance.deploy_node, 'fakeimg', '/fake/file') + + def test_delete_userid(self): + resp = {'error': [['Return Code: 400\nReason Code: 16\n']]} + + self.mox.StubOutWithMock(zvmutils, 'xcat_request') + self.mox.StubOutWithMock(self._instance, '_wait_for_unlock') + zvmutils.xcat_request("DELETE", mox.IgnoreArg()).AndRaise( + exception.ZVMXCATInternalError(msg=str(resp))) + self._instance._wait_for_unlock('fakehcp') + zvmutils.xcat_request("DELETE", mox.IgnoreArg()) + self.mox.ReplayAll() + + self._instance.delete_userid('fakehcp') + self.mox.VerifyAll() + + def test_delete_userid_400012(self): + resp = {'error': [['Return Code: 400\nReason Code: 12\n']]} + + self.mox.StubOutWithMock(zvmutils, 'xcat_request') + self.mox.StubOutWithMock(self._instance, '_wait_for_unlock') + zvmutils.xcat_request("DELETE", mox.IgnoreArg()).AndRaise( + exception.ZVMXCATInternalError(msg=str(resp))) + self._instance._wait_for_unlock('fakehcp') + zvmutils.xcat_request("DELETE", mox.IgnoreArg()) + self.mox.ReplayAll() + + self._instance.delete_userid('fakehcp') + self.mox.VerifyAll() + + def test_is_locked_true(self): + resp = {'data': [['os000001: os000001 is locked']]} + + self.mox.StubOutWithMock(zvmutils, 'xdsh') + zvmutils.xdsh('fakehcp', + "smcli Image_Lock_Query_DM -T os000001").AndReturn(resp) + self.mox.ReplayAll() + + locked = self._instance.is_locked('fakehcp') + self.mox.VerifyAll() + + self.assertTrue(locked) + + def test_is_locked_false(self): + resp = {'data': [['os000001: os000001 is Unlocked...']]} + + self.mox.StubOutWithMock(zvmutils, 'xdsh') + zvmutils.xdsh('fakehcp', + "smcli Image_Lock_Query_DM -T os000001").AndReturn(resp) + self.mox.ReplayAll() + + locked = self._instance.is_locked('fakehcp') + self.mox.VerifyAll() + + self.assertFalse(locked) + + def test_wait_for_unlock(self): + self.mox.StubOutWithMock(self._instance, 'is_locked') + self._instance.is_locked('fakehcp').AndReturn(True) + self._instance.is_locked('fakehcp').AndReturn(False) + self.mox.ReplayAll() + + self._instance._wait_for_unlock('fakehcp', 1) + self.mox.VerifyAll() + + +class ZVMXCATConnectionTestCases(test.TestCase): + """Test cases for xCAT connection.""" + + def setUp(self): + super(ZVMXCATConnectionTestCases, self).setUp() + self.flags(zvm_xcat_server='10.10.10.10', + zvm_xcat_username='fake', + zvm_xcat_password='fake') + + def _set_fake_response(self, response): + self.mox.StubOutWithMock(httplib.HTTPSConnection, 'request') + httplib.HTTPSConnection.request(mox.IgnoreArg(), mox.IgnoreArg(), + mox.IgnoreArg(), mox.IgnoreArg()) + self.mox.StubOutWithMock(httplib.HTTPSConnection, 'getresponse') + httplib.HTTPSConnection.getresponse().AndReturn(response) + self.mox.ReplayAll() + + def test_get(self): + self._set_fake_response(FakeHTTPResponse(200, 'OK', 'fake')) + conn = zvmutils.XCATConnection() + conn.request("GET", 'fakeurl') + self.mox.VerifyAll() + + def test_get_failed(self): + self._set_fake_response(FakeHTTPResponse(201, 'OK', 'fake')) + conn = zvmutils.XCATConnection() + self.assertRaises(exception.ZVMXCATRequestFailed, conn.request, + "GET", 'fakeurl') + self.mox.VerifyAll() + + def test_post(self): + self._set_fake_response(FakeHTTPResponse(201, 'Created', 'fake')) + conn = zvmutils.XCATConnection() + conn.request("POST", 'fakeurl') + self.mox.VerifyAll() + + def test_post_failed(self): + self._set_fake_response(FakeHTTPResponse(200, 'OK', 'fake')) + conn = zvmutils.XCATConnection() + self.assertRaises(exception.ZVMXCATRequestFailed, conn.request, + "POST", 'fakeurl') + self.mox.VerifyAll() + + def test_invalid_url(self): + self.mox.StubOutWithMock(httplib.HTTPSConnection, 'request') + httplib.HTTPSConnection.request(mox.IgnoreArg(), mox.IgnoreArg(), + mox.IgnoreArg(), mox.IgnoreArg()).AndRaise(socket.gaierror()) + self.mox.ReplayAll() + conn = zvmutils.XCATConnection() + self.assertRaises(exception.ZVMXCATRequestFailed, + conn.request, "GET", 'fakeurl') + + +class ZVMNetworkTestCases(ZVMTestCase): + """Test cases for network operator.""" + + def setUp(self): + super(ZVMNetworkTestCases, self).setUp() + self.networkop = networkop.NetworkOperator() + self.iname = self.instance['name'] + + def test_config_xcat_mac(self): + self._set_fake_xcat_responses([{'data': [{'data': ['mac']}]}]) + self.networkop.config_xcat_mac(self.iname) + + def test_add_xcat_host(self): + self._set_fake_xcat_responses([{'data': [{'data': ['mac']}]}]) + self.networkop.add_xcat_host(self.iname, '11.11.11.11', self.iname) + + def test_makehosts(self): + self._set_fake_xcat_responses([{'data': [{'data': ['mac']}]}]) + self.networkop.makehosts() + + def test_add_instance_nic(self): + self._set_fake_xcat_responses([{'data': [{'data': ['Done']}]}]) + self.networkop._add_instance_nic('fakehcp', self.iname, '1000', 'fake') + self.mox.VerifyAll() + + def test_add_instance_err(self): + self._set_fake_xcat_responses([{'data': [{'error': ['Error: err']}]}]) + self.assertRaises(exception.ZVMNetworkError, + self.networkop._add_instance_nic, 'fakehcp', + self.iname, '1000', 'fake') + self.mox.VerifyAll() + + def test_add_instance_err_and_warn(self): + self._set_fake_xcat_responses( + [{'data': [ + {'error': ['Error: err']}, + {'error': ['Warning: Permanently added zhcp host']} + ]}]) + self.assertRaises(exception.ZVMNetworkError, + self.networkop._add_instance_nic, 'fakehcp', + self.iname, '1000', 'fake') + self.mox.VerifyAll() + + def test_add_instance_ignore_warn(self): + self._set_fake_xcat_responses( + [{'data': [ + {'data': [{'error': ['Warning: Permanently added zhcp host']}]} + ]}]) + self.networkop._add_instance_nic('fakehcp', self.iname, '1000', 'fake') + self.mox.VerifyAll() + + +class ZVMUtilsTestCases(ZVMTestCase): + + def setUp(self): + super(ZVMUtilsTestCases, self).setUp() + + def test_generate_eph_vdev(self): + vdev0 = zvmutils.generate_eph_vdev(0) + vdev1 = zvmutils.generate_eph_vdev(1) + vdev2 = zvmutils.generate_eph_vdev(253) + self.assertEqual(vdev0, '0102') + self.assertEqual(vdev1, '0103') + self.assertEqual(vdev2, '01ff') + self.assertRaises(exception.ZVMDriverError, + zvmutils.generate_eph_vdev, -1) + self.assertRaises(exception.ZVMDriverError, + zvmutils.generate_eph_vdev, 254) + + def test__log_warnings(self): + resp = {'info': [''], + 'data': [], + 'node': ['Warn ...'], + 'error': []} + self.mox.StubOutWithMock(zvmutils.LOG, 'warn') + zvmutils.LOG.warn(_("Warning from xCAT: %s") % str(resp['node'])) + self.mox.ReplayAll() + + zvmutils._log_warnings(resp) + self.mox.VerifyAll() + + def test_parse_os_version(self): + fake_os = {'rhel': ['rhelx.y', 'redhatx.y', 'red hatx.y'], + 'sles': ['susex.y', 'slesx.y']} + for distro, patterns in fake_os.items(): + for i in patterns: + os, version = zvmutils.parse_os_version(i) + self.assertEqual(os, distro) + self.assertEqual(version, 'x.y') + + def test_parse_os_version_exception(self): + self.assertRaises(exception.ZVMImageError, + zvmutils.parse_os_version, + 'ubuntu') + + +class ZVMConfigDriveTestCase(test.NoDBTestCase): + + def setUp(self): + super(ZVMConfigDriveTestCase, self).setUp() + self.flags(config_drive_format='tgz', + tempdir='/tmp/os') + self.inst_md = FakeInstMeta() + + def test_create_configdrive_tgz(self): + self._file_path = CONF.tempdir + fileutils.ensure_tree(self._file_path) + self._file_name = self._file_path + '/configdrive.tgz' + + try: + with configdrive.ZVMConfigDriveBuilder( + instance_md=self.inst_md) as c: + c.make_drive(self._file_name) + + self.assertTrue(os.path.exists(self._file_name)) + + finally: + fileutils.remove_path_on_error(self._file_path) + + def test_make_tgz(self): + self._file_path = CONF.tempdir + fileutils.ensure_tree(self._file_path) + self._file_name = self._file_path + '/configdrive.tgz' + + self.mox.StubOutWithMock(os, 'getcwd') + os.getcwd().AndRaise(OSError('msg')) + os.getcwd().AndReturn(self._file_path) + os.getcwd().AndReturn(self._file_path) + self.mox.ReplayAll() + + try: + with configdrive.ZVMConfigDriveBuilder( + instance_md=self.inst_md) as c: + c.make_drive(self._file_name) + finally: + fileutils.remove_path_on_error(self._file_path) + + self.mox.VerifyAll() + + +class ZVMVolumeOperatorTestCase(ZVMTestCase): + + def setUp(self): + super(ZVMVolumeOperatorTestCase, self).setUp() + self.volumeop = volumeop.VolumeOperator() + self.mox.UnsetStubs() + + def test_init(self): + self.assertIsInstance(self.volumeop._svc_driver, volumeop.SVCDriver) + + def test_attach_volume_to_instance_check_args(self): + fake_connection_info = {'info': 'fake_info'} + fake_instance = {'name': 'fake_instance'} + fake_is_active = True + + farg = mox.IgnoreArg() + self.assertRaises(exception.ZVMDriverError, + self.volumeop.attach_volume_to_instance, None, + fake_connection_info, fake_instance, farg, None) + self.assertRaises(exception.ZVMDriverError, + self.volumeop.attach_volume_to_instance, None, + fake_connection_info, None, farg, fake_is_active) + self.assertRaises(exception.ZVMDriverError, + self.volumeop.attach_volume_to_instance, None, + None, fake_instance, farg, fake_is_active) + + def test_attach_volume_to_instance_active(self): + fake_connection_info = {'info': 'fake_info'} + fake_instance = {'name': 'fake_instance'} + is_active = True + + farg = mox.IgnoreArg() + self.mox.StubOutWithMock(self.volumeop._svc_driver, + 'attach_volume_active') + self.volumeop._svc_driver.attach_volume_active( + farg, farg, farg, farg, farg).AndReturn(None) + self.mox.ReplayAll() + + self.volumeop.attach_volume_to_instance(farg, fake_connection_info, + fake_instance, farg, is_active) + self.mox.VerifyAll() + + def test_attach_volume_to_instance_inactive(self): + fake_connection_info = {'info': 'fake_info'} + fake_instance = {'name': 'fake_instance'} + is_active = False + + farg = mox.IgnoreArg() + self.mox.StubOutWithMock(self.volumeop._svc_driver, + 'attach_volume_inactive') + self.volumeop._svc_driver.attach_volume_inactive( + farg, farg, farg, farg, farg).AndReturn(None) + self.mox.ReplayAll() + + self.volumeop.attach_volume_to_instance(farg, fake_connection_info, + fake_instance, farg, is_active) + self.mox.VerifyAll() + + def test_detach_volume_from_instance_active(self): + fake_connection_info = {'info': 'fake_info'} + fake_instance = {'name': 'fake_instance'} + is_active = True + + farg = mox.IgnoreArg() + self.mox.StubOutWithMock(self.volumeop._svc_driver, + 'detach_volume_active') + self.volumeop._svc_driver.detach_volume_active(farg, farg, farg, + farg).AndReturn(None) + self.mox.ReplayAll() + + self.volumeop.detach_volume_from_instance(fake_connection_info, + fake_instance, farg, + is_active) + self.mox.VerifyAll() + + def test_detach_volume_from_instance_inactive(self): + fake_connection_info = {'info': 'fake_info'} + fake_instance = {'name': 'fake_instance'} + is_active = False + + farg = mox.IgnoreArg() + self.mox.StubOutWithMock(self.volumeop._svc_driver, + 'detach_volume_inactive') + self.volumeop._svc_driver.detach_volume_inactive(farg, farg, farg, + farg).AndReturn(None) + self.mox.ReplayAll() + + self.volumeop.detach_volume_from_instance(fake_connection_info, + fake_instance, farg, + is_active) + self.mox.VerifyAll() + + def test_get_volume_connector_check_args(self): + self.assertRaises(exception.ZVMDriverError, + self.volumeop.get_volume_connector, None) + + +class SVCDriverTestCase(ZVMTestCase): + + def setUp(self): + super(SVCDriverTestCase, self).setUp() + self.driver = volumeop.SVCDriver() + self.mox.UnsetStubs() + + def test_init(self): + self.assertIsInstance(self.driver._xcat_url, zvmutils.XCATUrl) + self.assertIsInstance(self.driver._path_utils, zvmutils.PathUtils) + + def test_init_host_check_args(self): + self.assertRaises(exception.ZVMDriverError, + self.driver.init_host, None) + + def test_init_host_no_fcp(self): + fake_host_stats = [{'zhcp': {'nodename': 'fakename'}}] + self.flags(zvm_fcp_list=None) + + self.mox.StubOutWithMock(self.driver, '_expand_fcp_list') + self.driver._expand_fcp_list(mox.IgnoreArg()).AndReturn(set()) + self.mox.ReplayAll() + + self.assertRaises(exception.ZVMVolumeError, + self.driver.init_host, fake_host_stats) + self.mox.VerifyAll() + + def test_init_host(self): + fake_host_stats = [{'zhcp': {'nodename': 'fakename'}}] + self.mox.StubOutWithMock(self.driver, '_expand_fcp_list') + self.mox.StubOutWithMock(self.driver, '_attach_device') + self.mox.StubOutWithMock(self.driver, '_online_device') + self.mox.StubOutWithMock(self.driver, '_init_fcp_pool') + + farg = mox.IgnoreArg() + self.driver._expand_fcp_list(farg).AndReturn(['1FB0']) + self.driver._attach_device(farg, farg).AndReturn(None) + self.driver._online_device(farg, farg).AndReturn(None) + self.driver._init_fcp_pool(farg).AndReturn(None) + self.mox.ReplayAll() + + self.driver.init_host(fake_host_stats) + self.mox.VerifyAll() + + def test_init_fcp_pool(self): + fake_fcp_list = '0001-0003' + fake_instance = {'name': 'fake'} + fake_bdm = {'instance': fake_instance, 'instance_bdms': ['fake_bdm']} + fake_connection_info = {'data': {'zvm_fcp': '0002'}} + farg = mox.IgnoreArg() + + self.mox.StubOutWithMock(self.driver, '_expand_fcp_list') + self.mox.StubOutWithMock(self.driver, '_get_host_volume_bdms') + self.mox.StubOutWithMock(self.driver, '_build_connection_info') + self.driver._expand_fcp_list(farg). \ + AndReturn(set(['0001', '0002', '0003'])) + self.driver._get_host_volume_bdms().AndReturn([fake_bdm]) + self.driver._build_connection_info(farg). \ + AndReturn(fake_connection_info) + self.mox.ReplayAll() + + self.driver._init_fcp_pool(fake_fcp_list) + target_pool = set(['0001', '0003']) + self.assertTrue(self.driver._fcp_pool == target_pool) + self.assertEqual({'fcp': '0002', 'count': 1}, + self.driver._instance_fcp_map.get('fake')) + self.mox.VerifyAll() + self.driver._fcp_pool = set() + self.driver._instance_fcp_map = {} + + def test_build_connection_info_get_none(self): + self.assertIsNone(self.driver._build_connection_info(None)) + self.assertIsNone(self.driver._build_connection_info({'fake': 'bdm'})) + invalid_bdm = {"connection_info": 'aaa{"data": {"host": "fake_host",' + + '"zvm_fcp": "0001"}}'} + self.assertIsNone(self.driver._build_connection_info(invalid_bdm)) + + def test_build_connection_info(self): + fake_bdm = {"connection_info": '{"data": {"host": "fake_host",' + + '"zvm_fcp": "0001"}}'} + target_info = {'data': {'host': 'fake_host', 'zvm_fcp': '0001'}} + connection_info = self.driver._build_connection_info(fake_bdm) + self.assertTrue(connection_info == target_info) + + def test_get_volume_connector_no_fcp(self): + fake_instance = {'name': 'fake'} + self.mox.StubOutWithMock(self.driver, '_get_fcp_from_pool') + self.driver._get_fcp_from_pool().AndReturn(None) + self.mox.ReplayAll() + + self.assertRaises(exception.ZVMVolumeError, + self.driver.get_volume_connector, fake_instance) + self.mox.VerifyAll() + + def test_get_volume_connector_from_instance(self): + fake_instance = {'name': 'fake'} + self.driver._instance_fcp_map = {'fake': {'fcp': '1faa', 'count': 1}} + + farg = mox.IgnoreArg() + self.mox.StubOutWithMock(self.driver, '_get_wwpn') + self.driver._get_wwpn(farg).AndReturn('00000001') + self.mox.ReplayAll() + + target_connector = {'host': 'fakenode', + 'wwpns': ['00000001'], + 'zvm_fcp': '1faa'} + self.assertEqual(target_connector, + self.driver.get_volume_connector(fake_instance)) + self.mox.VerifyAll() + + self.driver._instance_fcp_map = {} + + def test_get_volume_connector_from_pool(self): + fake_instance = {'name': 'fake'} + self.mox.StubOutWithMock(self.driver, '_get_fcp_from_pool') + self.mox.StubOutWithMock(self.driver, '_get_wwpn') + farg = mox.IgnoreArg() + self.driver._get_fcp_from_pool().AndReturn('1FAA') + self.driver._get_wwpn(farg).AndReturn('00000001') + self.mox.ReplayAll() + + target_connector = {'host': 'fakenode', + 'wwpns': ['00000001'], + 'zvm_fcp': '1faa'} + self.assertEqual(target_connector, + self.driver.get_volume_connector(fake_instance)) + self.assertEqual({'fcp': '1faa', 'count': 0}, + self.driver._instance_fcp_map.get('fake')) + self.mox.VerifyAll() + + self.driver._instance_fcp_map = {} + + def test_get_volume_connector_no_wwpn(self): + fake_instance = {'name': 'fake'} + self.mox.StubOutWithMock(self.driver, '_get_fcp_from_pool') + self.mox.StubOutWithMock(self.driver, '_get_wwpn') + farg = mox.IgnoreArg() + self.driver._get_fcp_from_pool().AndReturn('0001') + self.driver._get_wwpn(farg).AndReturn(None) + self.mox.ReplayAll() + + self.assertRaises(exception.ZVMVolumeError, + self.driver.get_volume_connector, fake_instance) + self.mox.VerifyAll() + + def test_get_wwpn_get_none(self): + self.mox.StubOutWithMock(self.driver, '_list_fcp_details') + self.mox.StubOutWithMock(self.driver, '_extract_wwpn_from_fcp_info') + + farg = mox.IgnoreArg() + self.driver._list_fcp_details(farg).AndReturn('fake_active') + self.driver._extract_wwpn_from_fcp_info(farg, farg).AndReturn(None) + self.driver._list_fcp_details(farg).AndReturn('fake_free') + self.driver._extract_wwpn_from_fcp_info(farg, farg).AndReturn(None) + self.mox.ReplayAll() + + self.assertIsNone(self.driver._get_wwpn('1FAA')) + self.mox.VerifyAll() + + def test_get_wwpn(self): + self.mox.StubOutWithMock(self.driver, '_list_fcp_details') + self.mox.StubOutWithMock(self.driver, '_extract_wwpn_from_fcp_info') + + farg = mox.IgnoreArg() + self.driver._list_fcp_details(farg).AndReturn('fake_active') + self.driver._extract_wwpn_from_fcp_info(farg, farg).AndReturn(None) + self.driver._list_fcp_details(farg).AndReturn('fake_free') + self.driver._extract_wwpn_from_fcp_info(farg, farg).AndReturn( + '20076D8500003C11') + self.mox.ReplayAll() + + self.assertEqual(self.driver._get_wwpn('1FBC'), '20076D8500003C11') + self.mox.VerifyAll() + + def test_list_fcp_details_get_none(self): + self.mox.StubOutWithMock(self.driver, '_xcat_rinv') + self.driver._xcat_rinv(mox.IgnoreArg()).AndReturn({'info': None}) + self.mox.ReplayAll() + + self.assertIsNone(self.driver._list_fcp_details('free')) + self.mox.VerifyAll() + + def test_list_fcp_details(self): + fake_rsp = {'info': [['line1\nline2\nline3\nline4\nline5\n']]} + self.mox.StubOutWithMock(self.driver, '_xcat_rinv') + self.driver._xcat_rinv(mox.IgnoreArg()).AndReturn(fake_rsp) + self.mox.ReplayAll() + + target = ['line1', 'line2', 'line3', 'line4', 'line5'] + self.assertTrue(target == self.driver._list_fcp_details('active')) + self.mox.VerifyAll() + + def test_extract_wwpn_from_fcp_info_get_none(self): + line1 = 'opnstk1: FCP device number: 1FAC' + line2 = 'opnstk1: Status: Active' + line3 = 'opnstk1: NPIV world wide port number: NONE' + line4 = 'opnstk1: Channel path ID: 16' + line5 = 'opnstk1: Physical world wide port number: 20076D8500003C11' + line6 = 'opnstk1: FCP device number: 1FAD' + line7 = 'opnstk1: Status: Active' + line8 = 'opnstk1: NPIV world wide port number: NONE' + line9 = 'opnstk1: Channel path ID: 16' + lineA = 'opnstk1: Physical world wide port number: 20076D8500003C12' + fake_fcp_info = [line1, line2, line3, line4, line5, + line6, line7, line8, line9, lineA] + self.assertIsNone(self.driver._extract_wwpn_from_fcp_info( + '1FBC', fake_fcp_info)) + + def test_extract_wwpn_from_fcp_info_NPIV_wwpn(self): + line1 = 'opnstk1: FCP device number: 1FAC' + line2 = 'opnstk1: Status: Active' + line3 = 'opnstk1: NPIV world wide port number: 20076D8500003C00' + line4 = 'opnstk1: Channel path ID: 16' + line5 = 'opnstk1: Physical world wide port number: 20076D8500003C11' + line6 = 'opnstk1: FCP device number: 1FAD' + line7 = 'opnstk1: Status: Active' + line8 = 'opnstk1: NPIV world wide port number: 20076D8500003C01' + line9 = 'opnstk1: Channel path ID: 16' + lineA = 'opnstk1: Physical world wide port number: 20076D8500003C11' + fake_fcp_info = [line1, line2, line3, line4, line5, + line6, line7, line8, line9, lineA] + target_wwpn = '20076D8500003C01' + self.assertEqual(self.driver._extract_wwpn_from_fcp_info( + '1FAD', fake_fcp_info), target_wwpn) + + def test_extract_wwpn_from_fcp_info_physical_wwpn(self): + line1 = 'opnstk1: FCP device number: 1FAC' + line2 = 'opnstk1: Status: Active' + line3 = 'opnstk1: NPIV world wide port number: NONE' + line4 = 'opnstk1: Channel path ID: 16' + line5 = 'opnstk1: Physical world wide port number: 20076D8500003C10' + line6 = 'opnstk1: FCP device number: 1FAD' + line7 = 'opnstk1: Status: Active' + line8 = 'opnstk1: NPIV world wide port number: NONE' + line9 = 'opnstk1: Channel path ID: 16' + lineA = 'opnstk1: Physical world wide port number: 20076D8500003C11' + fake_fcp_info = [line1, line2, line3, line4, line5, + line6, line7, line8, line9, lineA] + target_wwpn = '20076D8500003C11' + self.assertEqual(self.driver._extract_wwpn_from_fcp_info( + '1FAD', fake_fcp_info), target_wwpn) + + def test_get_wwpn_from_line_get_none(self): + wwpn_info = 'opnstk1: NPIV world wide port number: NONE' + self.assertIsNone(self.driver._get_wwpn_from_line(wwpn_info)) + wwpn_info = 'opnstk1: NPIV world wide port number: none' + self.assertIsNone(self.driver._get_wwpn_from_line(wwpn_info)) + wwpn_info = 'opnstk1: NPIV world wide port number: ' + self.assertIsNone(self.driver._get_wwpn_from_line(wwpn_info)) + + def test_get_wwpn_from_line(self): + wwpn_info = 'opnstk1: NPIV world wide port number: 20076D8500003C00' + target_wwpn = '20076D8500003C00' + self.assertEqual(self.driver._get_wwpn_from_line(wwpn_info), + target_wwpn) + + def test_get_fcp_from_pool_get_none(self): + self.driver._fcp_pool = set() + self.mox.StubOutWithMock(self.driver, '_init_fcp_pool') + self.driver._init_fcp_pool(mox.IgnoreArg()).AndReturn(None) + self.mox.ReplayAll() + self.assertIsNone(self.driver._get_fcp_from_pool()) + self.mox.VerifyAll() + + def test_get_fcp_from_pool_no_init(self): + self.driver._fcp_pool = set(['1FAA']) + self.assertEqual(self.driver._get_fcp_from_pool(), '1FAA') + self.driver._fcp_pool = set() + + def test_get_fcp_from_pool_do_init(self): + def set_fcp_pool(*params, **named_params): + self.driver._fcp_pool = set(['1FAA']) + + self.mox.StubOutWithMock(self.driver, '_init_fcp_pool') + self.driver._init_fcp_pool(mox.IgnoreArg()).WithSideEffects( + set_fcp_pool) + self.mox.ReplayAll() + self.assertEqual(self.driver._get_fcp_from_pool(), '1FAA') + self.mox.VerifyAll() + self.driver._fcp_pool = set() + + def test_extract_connection_info_error(self): + fake_connection_info = 'fake_info' + self.assertRaises(exception.ZVMVolumeError, + self.driver._extract_connection_info, + None, fake_connection_info) + + def test_extract_connection_info_no_context(self): + fake_connection_info = {'data': {'target_lun': 10, + 'target_wwn': '0000000B', + 'zvm_fcp': '1FAA'}} + target_info = ('000a000000000000', '0000000b', '0G', '1faa') + self.assertEqual(target_info, + self.driver._extract_connection_info( + None, fake_connection_info)) + + def test_extract_connection_info_with_context(self): + fake_connection_info = {'data': {'target_lun': 10, + 'target_wwn': '0000000B', + 'volume_id': 'fake_id', + 'zvm_fcp': '1FAA'}} + fake_context = 'fake_context' + self.mox.StubOutWithMock(self.driver, '_get_volume_by_id') + + farg = mox.IgnoreArg() + self.driver._get_volume_by_id(farg, farg).AndReturn({'size': 2}) + self.mox.ReplayAll() + + target_info = ('000a000000000000', '0000000b', '2G', '1faa') + self.assertEqual(target_info, + self.driver._extract_connection_info( + fake_context, fake_connection_info)) + self.mox.VerifyAll() + + def test_attach_volume_active_error_no_rollback(self): + fake_info = ('lun', 'wwpn', '1G', '1faa') + fake_instance = {'name': 'fake'} + fake_mountpoint = '/dev/vdd' + self.driver._instance_fcp_map = {} + rollback = False + + self.mox.StubOutWithMock(self.driver, '_extract_connection_info') + self.mox.StubOutWithMock(self.driver, '_add_zfcp_to_pool') + self.mox.StubOutWithMock(self.driver, '_add_zfcp') + self.mox.StubOutWithMock(self.driver, '_create_mountpoint') + + farg = mox.IgnoreArg() + self.driver._extract_connection_info(farg, farg).AndReturn(fake_info) + self.driver._add_zfcp_to_pool(farg, farg, farg, farg).AndReturn(None) + self.driver._add_zfcp(farg, farg, farg, farg, farg).AndReturn(None) + self.driver._create_mountpoint(farg, farg, farg, farg, farg).AndRaise( + exception.ZVMVolumeError(msg='No msg')) + self.mox.ReplayAll() + + self.assertRaises(exception.ZVMVolumeError, + self.driver.attach_volume_active, farg, + farg, fake_instance, fake_mountpoint, rollback) + self.assertFalse(self.driver._is_fcp_in_use(fake_instance, '1faa')) + self.mox.VerifyAll() + self.driver._instance_fcp_map = {} + + def test_attach_volume_active_error_rollback_and_detach(self): + fake_info = ('lun', 'wwpn', '1G', '1faa') + fake_mountpoint = '/dev/vdd' + fake_instance = {'name': 'fake'} + self.driver._instance_fcp_map = {} + rollback = True + + self.mox.StubOutWithMock(self.driver, '_extract_connection_info') + self.mox.StubOutWithMock(self.driver, '_add_zfcp_to_pool') + self.mox.StubOutWithMock(self.driver, '_add_zfcp') + self.mox.StubOutWithMock(self.driver, '_create_mountpoint') + self.mox.StubOutWithMock(self.driver, '_remove_mountpoint') + self.mox.StubOutWithMock(self.driver, '_remove_zfcp') + self.mox.StubOutWithMock(self.driver, '_remove_zfcp_from_pool') + self.mox.StubOutWithMock(self.driver, '_detach_device') + + farg = mox.IgnoreArg() + self.driver._extract_connection_info(farg, farg).AndReturn(fake_info) + self.driver._add_zfcp_to_pool(farg, farg, farg, farg).AndReturn(None) + self.driver._add_zfcp(farg, farg, farg, farg, farg).AndReturn(None) + self.driver._create_mountpoint(farg, farg, farg, farg, farg).AndRaise( + exception.ZVMVolumeError(msg='No msg')) + self.driver._remove_mountpoint(farg, farg).AndRaise( + exception.ZVMVolumeError(msg='No msg')) + self.driver._remove_zfcp(farg, farg, farg, farg).AndRaise( + exception.ZVMVolumeError(msg='No msg')) + self.driver._remove_zfcp_from_pool(farg, farg).AndRaise( + exception.ZVMVolumeError(msg='No msg')) + self.driver._detach_device(farg, farg).AndReturn(None) + self.mox.ReplayAll() + + self.assertRaises(exception.ZVMVolumeError, + self.driver.attach_volume_active, farg, farg, + fake_instance, fake_mountpoint, rollback) + self.assertFalse(self.driver._is_fcp_in_use(fake_instance, '1faa')) + self.mox.VerifyAll() + self.driver._instance_fcp_map = {} + + def test_attach_volume_active_error_rollback_no_detach(self): + fake_info = ('lun', 'wwpn', '1G', '1faa') + fake_mountpoint = '/dev/vdd' + fake_instance = {'name': 'fake'} + self.driver._instance_fcp_map = {'fake': {'fcp': '1faa', 'count': 1}} + rollback = True + + self.mox.StubOutWithMock(self.driver, '_extract_connection_info') + self.mox.StubOutWithMock(self.driver, '_add_zfcp_to_pool') + self.mox.StubOutWithMock(self.driver, '_add_zfcp') + self.mox.StubOutWithMock(self.driver, '_create_mountpoint') + self.mox.StubOutWithMock(self.driver, '_remove_mountpoint') + self.mox.StubOutWithMock(self.driver, '_remove_zfcp') + self.mox.StubOutWithMock(self.driver, '_remove_zfcp_from_pool') + + farg = mox.IgnoreArg() + self.driver._extract_connection_info(farg, farg).AndReturn(fake_info) + self.driver._add_zfcp_to_pool(farg, farg, farg, farg).AndReturn(None) + self.driver._add_zfcp(farg, farg, farg, farg, farg).AndReturn(None) + self.driver._create_mountpoint(farg, farg, farg, farg, farg).AndRaise( + exception.ZVMVolumeError(msg='No msg')) + self.driver._remove_mountpoint(farg, farg).AndRaise( + exception.ZVMVolumeError(msg='No msg')) + self.driver._remove_zfcp(farg, farg, farg, farg).AndRaise( + exception.ZVMVolumeError(msg='No msg')) + self.driver._remove_zfcp_from_pool(farg, farg).AndRaise( + exception.ZVMVolumeError(msg='No msg')) + self.mox.ReplayAll() + + self.assertRaises(exception.ZVMVolumeError, + self.driver.attach_volume_active, farg, + farg, fake_instance, fake_mountpoint, rollback) + self.assertTrue(self.driver._is_fcp_in_use(fake_instance, '1faa')) + self.mox.VerifyAll() + self.driver._instance_fcp_map = {} + + def test_attach_volume_active(self): + fake_info = ('lun', 'wwpn', '1G', '1faa') + fake_mountpoint = '/dev/vdd' + fake_instance = {'name': 'fake'} + self.driver._instance_fcp_map = {} + rollback = True + + self.mox.StubOutWithMock(self.driver, '_extract_connection_info') + self.mox.StubOutWithMock(self.driver, '_add_zfcp_to_pool') + self.mox.StubOutWithMock(self.driver, '_add_zfcp') + self.mox.StubOutWithMock(self.driver, '_create_mountpoint') + + farg = mox.IgnoreArg() + self.driver._extract_connection_info(farg, farg).AndReturn(fake_info) + self.driver._add_zfcp_to_pool(farg, farg, farg, farg).AndReturn(None) + self.driver._add_zfcp(farg, farg, farg, farg, farg).AndReturn(None) + self.driver._create_mountpoint(farg, farg, farg, farg, farg).AndReturn( + None) + self.mox.ReplayAll() + + self.driver.attach_volume_active(farg, farg, fake_instance, + fake_mountpoint, rollback) + self.assertTrue(self.driver._is_fcp_in_use(fake_instance, '1faa')) + self.mox.VerifyAll() + self.driver._instance_fcp_map = {} + + def test_attach_volume_active_no_mountpoint(self): + fake_info = ('lun', 'wwpn', '1G', '1faa') + fake_instance = {'name': 'fake'} + self.driver._instance_fcp_map = {} + rollback = True + + self.mox.StubOutWithMock(self.driver, '_extract_connection_info') + self.mox.StubOutWithMock(self.driver, '_add_zfcp_to_pool') + self.mox.StubOutWithMock(self.driver, '_add_zfcp') + self.mox.StubOutWithMock(self.driver, '_create_mountpoint') + + farg = mox.IgnoreArg() + self.driver._extract_connection_info(farg, farg).AndReturn(fake_info) + self.driver._add_zfcp_to_pool(farg, farg, farg, farg).AndReturn(None) + self.driver._add_zfcp(farg, farg, farg, farg, farg).AndReturn(None) + self.mox.ReplayAll() + + self.driver.attach_volume_active(farg, farg, fake_instance, + None, rollback) + self.assertTrue(self.driver._is_fcp_in_use(fake_instance, '1faa')) + self.mox.VerifyAll() + self.driver._instance_fcp_map = {} + + def test_detach_volume_active_error_no_rollback(self): + fake_info = ('lun', 'wwpn', '1G', '1faa') + fake_instance = {'name': 'fake'} + fake_mountpoint = '/dev/vdd' + self.driver._instance_fcp_map = {'fake': {'fcp': '1faa', 'count': 1}} + rollback = False + + self.mox.StubOutWithMock(self.driver, '_extract_connection_info') + self.mox.StubOutWithMock(self.driver, '_remove_mountpoint') + self.mox.StubOutWithMock(self.driver, '_remove_zfcp') + self.mox.StubOutWithMock(self.driver, '_remove_zfcp_from_pool') + self.mox.StubOutWithMock(self.driver, '_detach_device') + + farg = mox.IgnoreArg() + self.driver._extract_connection_info(farg, farg).AndReturn(fake_info) + self.driver._remove_mountpoint(farg, farg).AndReturn(None) + self.driver._remove_zfcp(farg, farg, farg, farg).AndReturn(None) + self.driver._remove_zfcp_from_pool(farg, farg).AndReturn(None) + self.driver._detach_device(farg, farg).AndRaise( + exception.ZVMVolumeError(msg='No msg')) + self.mox.ReplayAll() + + self.assertRaises(exception.ZVMVolumeError, + self.driver.detach_volume_active, farg, + fake_instance, fake_mountpoint, rollback) + self.assertTrue(self.driver._is_fcp_in_use(fake_instance, '1faa')) + self.mox.VerifyAll() + self.driver._instance_fcp_map = {} + + def test_detach_volume_active_error_rollback_with_mountpoint(self): + fake_info = ('lun', 'wwpn', '1G', '1faa') + fake_instance = {'name': 'fake'} + fake_mountpoint = '/dev/vdd' + self.driver._instance_fcp_map = {'fake': {'fcp': '1faa', 'count': 1}} + rollback = True + + self.mox.StubOutWithMock(self.driver, '_extract_connection_info') + self.mox.StubOutWithMock(self.driver, '_remove_mountpoint') + self.mox.StubOutWithMock(self.driver, '_remove_zfcp') + self.mox.StubOutWithMock(self.driver, '_remove_zfcp_from_pool') + self.mox.StubOutWithMock(self.driver, '_detach_device') + self.mox.StubOutWithMock(self.driver, '_add_zfcp_to_pool') + self.mox.StubOutWithMock(self.driver, '_add_zfcp') + self.mox.StubOutWithMock(self.driver, '_create_mountpoint') + + farg = mox.IgnoreArg() + self.driver._extract_connection_info(farg, farg).AndReturn(fake_info) + self.driver._remove_mountpoint(farg, farg).AndReturn(None) + self.driver._remove_zfcp(farg, farg, farg, farg).AndReturn(None) + self.driver._remove_zfcp_from_pool(farg, farg).AndReturn(None) + self.driver._detach_device(farg, farg).AndRaise( + exception.ZVMVolumeError(msg='No msg')) + self.driver._add_zfcp_to_pool(farg, farg, farg, farg).AndRaise( + exception.ZVMVolumeError(msg='No msg')) + self.driver._add_zfcp(farg, farg, farg, farg, farg).AndRaise( + exception.ZVMVolumeError(msg='No msg')) + self.driver._create_mountpoint(farg, farg, farg, farg, farg).AndReturn( + None) + self.mox.ReplayAll() + + self.assertRaises(exception.ZVMVolumeError, + self.driver.detach_volume_active, farg, + fake_instance, fake_mountpoint, rollback) + self.assertTrue(self.driver._is_fcp_in_use(fake_instance, '1faa')) + self.mox.VerifyAll() + self.driver._instance_fcp_map = {} + + def test_detach_volume_active_error_rollback_no_mountpoint(self): + fake_info = ('lun', 'wwpn', '1G', '1faa') + fake_instance = {'name': 'fake'} + self.driver._instance_fcp_map = {'fake': {'fcp': '1faa', 'count': 1}} + rollback = True + + self.mox.StubOutWithMock(self.driver, '_extract_connection_info') + self.mox.StubOutWithMock(self.driver, '_remove_zfcp') + self.mox.StubOutWithMock(self.driver, '_remove_zfcp_from_pool') + self.mox.StubOutWithMock(self.driver, '_detach_device') + self.mox.StubOutWithMock(self.driver, '_add_zfcp_to_pool') + self.mox.StubOutWithMock(self.driver, '_add_zfcp') + + farg = mox.IgnoreArg() + self.driver._extract_connection_info(farg, farg).AndReturn(fake_info) + self.driver._remove_zfcp(farg, farg, farg, farg).AndReturn(None) + self.driver._remove_zfcp_from_pool(farg, farg).AndReturn(None) + self.driver._detach_device(farg, farg).AndRaise( + exception.ZVMVolumeError(msg='No msg')) + self.driver._add_zfcp_to_pool(farg, farg, farg, farg).AndRaise( + exception.ZVMVolumeError(msg='No msg')) + self.driver._add_zfcp(farg, farg, farg, farg, farg).AndReturn(None) + self.mox.ReplayAll() + + self.assertRaises(exception.ZVMVolumeError, + self.driver.detach_volume_active, + farg, fake_instance, None, rollback) + self.assertTrue(self.driver._is_fcp_in_use(fake_instance, '1faa')) + self.mox.VerifyAll() + self.driver._instance_fcp_map = {} + + def test_detach_volume_active_and_detach_fcp(self): + fake_info = ('lun', 'wwpn', '1G', '1faa') + fake_mountpoint = '/dev/vdd' + fake_instance = {'name': 'fake'} + self.driver._instance_fcp_map = {'fake': {'fcp': '1faa', 'count': 1}} + rollback = False + + self.mox.StubOutWithMock(self.driver, '_extract_connection_info') + self.mox.StubOutWithMock(self.driver, '_remove_mountpoint') + self.mox.StubOutWithMock(self.driver, '_remove_zfcp') + self.mox.StubOutWithMock(self.driver, '_remove_zfcp_from_pool') + self.mox.StubOutWithMock(self.driver, '_detach_device') + + farg = mox.IgnoreArg() + self.driver._extract_connection_info(farg, farg).AndReturn(fake_info) + self.driver._remove_mountpoint(farg, farg).AndReturn(None) + self.driver._remove_zfcp(farg, farg, farg, farg).AndReturn(None) + self.driver._remove_zfcp_from_pool(farg, farg).AndReturn(None) + self.driver._detach_device(farg, farg).AndReturn(None) + self.mox.ReplayAll() + + self.driver.detach_volume_active(farg, fake_instance, + fake_mountpoint, rollback) + self.assertFalse(self.driver._is_fcp_in_use(fake_instance, '1faa')) + self.mox.VerifyAll() + self.driver._instance_fcp_map = {} + + def test_detach_volume_active_and_reserve_fcp(self): + fake_info = ('lun', 'wwpn', '1G', '1faa') + fake_mountpoint = '/dev/vdd' + fake_instance = {'name': 'fake'} + self.driver._instance_fcp_map = {'fake': {'fcp': '1faa', 'count': 2}} + rollback = False + + self.mox.StubOutWithMock(self.driver, '_extract_connection_info') + self.mox.StubOutWithMock(self.driver, '_remove_mountpoint') + self.mox.StubOutWithMock(self.driver, '_remove_zfcp') + self.mox.StubOutWithMock(self.driver, '_remove_zfcp_from_pool') + + farg = mox.IgnoreArg() + self.driver._extract_connection_info(farg, farg).AndReturn(fake_info) + self.driver._remove_mountpoint(farg, farg).AndReturn(None) + self.driver._remove_zfcp(farg, farg, farg, farg).AndReturn(None) + self.driver._remove_zfcp_from_pool(farg, farg).AndReturn(None) + self.mox.ReplayAll() + + self.driver.detach_volume_active(farg, fake_instance, + fake_mountpoint, rollback) + self.assertTrue(self.driver._is_fcp_in_use(fake_instance, '1faa')) + self.mox.VerifyAll() + self.driver._instance_fcp_map = {} + + def test_attach_volume_inactive_error_no_rollback(self): + fake_info = ('lun', 'wwpn', '1G', '1faa') + fake_instance = {'name': 'fake'} + self.driver._instance_fcp_map = {} + rollback = False + + self.mox.StubOutWithMock(self.driver, '_extract_connection_info') + self.mox.StubOutWithMock(self.driver, '_add_zfcp_to_pool') + self.mox.StubOutWithMock(self.driver, '_allocate_zfcp') + self.mox.StubOutWithMock(self.driver, '_notice_attach') + self.mox.StubOutWithMock(self.driver, '_attach_device') + + farg = mox.IgnoreArg() + self.driver._extract_connection_info(farg, farg).AndReturn(fake_info) + self.driver._add_zfcp_to_pool(farg, farg, farg, farg).AndReturn(None) + self.driver._allocate_zfcp(farg, farg, farg, farg, farg).AndReturn( + None) + self.driver._notice_attach(farg, farg, farg, farg, farg).AndReturn( + None) + self.driver._attach_device(farg, farg).AndRaise( + exception.ZVMVolumeError(msg='No msg')) + self.mox.ReplayAll() + + self.assertRaises(exception.ZVMVolumeError, + self.driver.attach_volume_inactive, farg, + farg, fake_instance, farg, rollback) + self.assertFalse(self.driver._is_fcp_in_use(fake_instance, '1faa')) + self.mox.VerifyAll() + self.driver._instance_fcp_map = {} + + def test_attach_volume_inactive_error_rollback_and_detach(self): + fake_info = ('lun', 'wwpn', '1G', '1faa') + fake_instance = {'name': 'fake'} + self.driver._instance_fcp_map = {} + rollback = True + + self.mox.StubOutWithMock(self.driver, '_extract_connection_info') + self.mox.StubOutWithMock(self.driver, '_add_zfcp_to_pool') + self.mox.StubOutWithMock(self.driver, '_allocate_zfcp') + self.mox.StubOutWithMock(self.driver, '_notice_attach') + self.mox.StubOutWithMock(self.driver, '_attach_device') + self.mox.StubOutWithMock(self.driver, '_notice_detach') + self.mox.StubOutWithMock(self.driver, '_remove_zfcp_from_pool') + self.mox.StubOutWithMock(self.driver, '_detach_device') + + farg = mox.IgnoreArg() + self.driver._extract_connection_info(farg, farg).AndReturn(fake_info) + self.driver._add_zfcp_to_pool(farg, farg, farg, farg).AndReturn(None) + self.driver._allocate_zfcp(farg, farg, farg, farg, farg).AndReturn( + None) + self.driver._notice_attach(farg, farg, farg, farg, farg).AndReturn( + None) + self.driver._attach_device(farg, farg).AndRaise( + exception.ZVMVolumeError(msg='No msg')) + self.driver._notice_detach(farg, farg, farg, farg, farg).AndRaise( + exception.ZVMVolumeError(msg='No msg')) + self.driver._remove_zfcp_from_pool(farg, farg).AndRaise( + exception.ZVMVolumeError(msg='No msg')) + self.driver._detach_device(farg, farg).AndReturn(None) + self.mox.ReplayAll() + + self.assertRaises(exception.ZVMVolumeError, + self.driver.attach_volume_inactive, farg, + farg, fake_instance, farg, rollback) + self.assertFalse(self.driver._is_fcp_in_use(fake_instance, '1faa')) + self.mox.VerifyAll() + self.driver._instance_fcp_map = {} + + def test_attach_volume_inactive_error_rollback_no_detach(self): + fake_info = ('lun', 'wwpn', '1G', '1faa') + fake_instance = {'name': 'fake'} + self.driver._instance_fcp_map = {'fake': {'fcp': '1faa', 'count': 1}} + rollback = True + + self.mox.StubOutWithMock(self.driver, '_extract_connection_info') + self.mox.StubOutWithMock(self.driver, '_add_zfcp_to_pool') + self.mox.StubOutWithMock(self.driver, '_allocate_zfcp') + self.mox.StubOutWithMock(self.driver, '_notice_attach') + self.mox.StubOutWithMock(self.driver, '_notice_detach') + self.mox.StubOutWithMock(self.driver, '_remove_zfcp_from_pool') + + farg = mox.IgnoreArg() + self.driver._extract_connection_info(farg, farg).AndReturn(fake_info) + self.driver._add_zfcp_to_pool(farg, farg, farg, farg).AndReturn(None) + self.driver._allocate_zfcp(farg, farg, farg, farg, farg).AndReturn( + None) + self.driver._notice_attach(farg, farg, farg, farg, farg).AndRaise( + exception.ZVMVolumeError(msg='No msg')) + self.driver._notice_detach(farg, farg, farg, farg, farg).AndRaise( + exception.ZVMVolumeError(msg='No msg')) + self.driver._remove_zfcp_from_pool(farg, farg).AndRaise( + exception.ZVMVolumeError(msg='No msg')) + self.mox.ReplayAll() + + self.assertRaises(exception.ZVMVolumeError, + self.driver.attach_volume_inactive, farg, + farg, fake_instance, farg, rollback) + self.assertTrue(self.driver._is_fcp_in_use(fake_instance, '1faa')) + self.mox.VerifyAll() + self.driver._instance_fcp_map = {} + + def test_attach_volume_inactive(self): + fake_info = ('lun', 'wwpn', '1G', '1faa') + fake_instance = {'name': 'fake'} + self.driver._instance_fcp_map = {} + rollback = True + + self.mox.StubOutWithMock(self.driver, '_extract_connection_info') + self.mox.StubOutWithMock(self.driver, '_add_zfcp_to_pool') + self.mox.StubOutWithMock(self.driver, '_allocate_zfcp') + self.mox.StubOutWithMock(self.driver, '_notice_attach') + self.mox.StubOutWithMock(self.driver, '_attach_device') + + farg = mox.IgnoreArg() + self.driver._extract_connection_info(farg, farg).AndReturn(fake_info) + self.driver._add_zfcp_to_pool(farg, farg, farg, farg).AndReturn(None) + self.driver._allocate_zfcp(farg, farg, farg, farg, farg).AndReturn( + None) + self.driver._notice_attach(farg, farg, farg, farg, farg).AndReturn( + None) + self.driver._attach_device(farg, farg).AndReturn(None) + self.mox.ReplayAll() + + self.driver.attach_volume_inactive(farg, farg, fake_instance, + farg, rollback) + self.assertTrue(self.driver._is_fcp_in_use(fake_instance, '1faa')) + self.mox.VerifyAll() + self.driver._instance_fcp_map = {} + + def test_attach_volume_inactive_no_attach(self): + fake_info = ('lun', 'wwpn', '1G', '1faa') + fake_instance = {'name': 'fake'} + self.driver._instance_fcp_map = {'fake': {'fcp': '1faa', 'count': 1}} + rollback = True + + self.mox.StubOutWithMock(self.driver, '_extract_connection_info') + self.mox.StubOutWithMock(self.driver, '_add_zfcp_to_pool') + self.mox.StubOutWithMock(self.driver, '_allocate_zfcp') + self.mox.StubOutWithMock(self.driver, '_notice_attach') + + farg = mox.IgnoreArg() + self.driver._extract_connection_info(farg, farg).AndReturn(fake_info) + self.driver._add_zfcp_to_pool(farg, farg, farg, farg).AndReturn(None) + self.driver._allocate_zfcp(farg, farg, farg, farg, farg).AndReturn( + None) + self.driver._notice_attach(farg, farg, farg, farg, farg).AndReturn( + None) + self.mox.ReplayAll() + + self.driver.attach_volume_inactive(farg, farg, fake_instance, + farg, rollback) + self.assertTrue(self.driver._is_fcp_in_use(fake_instance, '1faa')) + self.mox.VerifyAll() + self.driver._instance_fcp_map = {} + + def test_detach_volume_inactive_error_no_rollback(self): + fake_info = ('lun', 'wwpn', '1G', '1faa') + fake_instance = {'name': 'fake'} + self.driver._instance_fcp_map = {'fake': {'fcp': '1faa', 'count': 1}} + rollback = False + + self.mox.StubOutWithMock(self.driver, '_extract_connection_info') + self.mox.StubOutWithMock(self.driver, '_remove_zfcp') + self.mox.StubOutWithMock(self.driver, '_remove_zfcp_from_pool') + self.mox.StubOutWithMock(self.driver, '_notice_detach') + self.mox.StubOutWithMock(self.driver, '_detach_device') + + farg = mox.IgnoreArg() + self.driver._extract_connection_info(farg, farg).AndReturn(fake_info) + self.driver._remove_zfcp(farg, farg, farg, farg).AndReturn(None) + self.driver._remove_zfcp_from_pool(farg, farg).AndReturn(None) + self.driver._notice_detach(farg, farg, farg, farg, farg).AndReturn( + None) + self.driver._detach_device(farg, farg).AndRaise( + exception.ZVMVolumeError(msg='No msg')) + self.mox.ReplayAll() + + self.assertRaises(exception.ZVMVolumeError, + self.driver.detach_volume_inactive, + farg, fake_instance, farg, rollback) + self.assertTrue(self.driver._is_fcp_in_use(fake_instance, '1faa')) + self.mox.VerifyAll() + self.driver._instance_fcp_map = {} + + def test_detach_volume_inactive_error_detach_rollback(self): + fake_info = ('lun', 'wwpn', '1G', '1faa') + fake_instance = {'name': 'fake'} + self.driver._instance_fcp_map = {'fake': {'fcp': '1faa', 'count': 1}} + rollback = True + + self.mox.StubOutWithMock(self.driver, '_extract_connection_info') + self.mox.StubOutWithMock(self.driver, '_remove_zfcp') + self.mox.StubOutWithMock(self.driver, '_remove_zfcp_from_pool') + self.mox.StubOutWithMock(self.driver, '_notice_detach') + self.mox.StubOutWithMock(self.driver, '_detach_device') + self.mox.StubOutWithMock(self.driver, '_attach_device') + self.mox.StubOutWithMock(self.driver, '_notice_attach') + self.mox.StubOutWithMock(self.driver, '_add_zfcp_to_pool') + self.mox.StubOutWithMock(self.driver, '_allocate_zfcp') + + farg = mox.IgnoreArg() + self.driver._extract_connection_info(farg, farg).AndReturn(fake_info) + self.driver._remove_zfcp(farg, farg, farg, farg).AndReturn(None) + self.driver._remove_zfcp_from_pool(farg, farg).AndReturn(None) + self.driver._notice_detach(farg, farg, farg, farg, farg).AndReturn( + None) + self.driver._detach_device(farg, farg).AndRaise( + exception.ZVMVolumeError(msg='No msg')) + self.driver._attach_device(farg, farg).AndRaise( + exception.ZVMVolumeError(msg='No msg')) + self.driver._notice_attach(farg, farg, farg, farg, farg).AndRaise( + exception.ZVMVolumeError(msg='No msg')) + self.driver._add_zfcp_to_pool(farg, farg, farg, farg).AndReturn(True) + self.driver._allocate_zfcp(farg, farg, farg, farg, farg).AndReturn( + None) + self.mox.ReplayAll() + + self.assertRaises(exception.ZVMVolumeError, + self.driver.detach_volume_inactive, + farg, fake_instance, farg, rollback) + self.assertTrue(self.driver._is_fcp_in_use(fake_instance, '1faa')) + self.mox.VerifyAll() + self.driver._instance_fcp_map = {} + + def test_detach_volume_inactive_error_no_detach_rollback(self): + fake_info = ('lun', 'wwpn', '1G', '1faa') + fake_instance = {'name': 'fake'} + self.driver._instance_fcp_map = {'fake': {'fcp': '1faa', 'count': 2}} + rollback = True + + self.mox.StubOutWithMock(self.driver, '_extract_connection_info') + self.mox.StubOutWithMock(self.driver, '_remove_zfcp') + self.mox.StubOutWithMock(self.driver, '_remove_zfcp_from_pool') + self.mox.StubOutWithMock(self.driver, '_notice_detach') + self.mox.StubOutWithMock(self.driver, '_attach_device') + self.mox.StubOutWithMock(self.driver, '_notice_attach') + self.mox.StubOutWithMock(self.driver, '_add_zfcp_to_pool') + self.mox.StubOutWithMock(self.driver, '_allocate_zfcp') + + farg = mox.IgnoreArg() + self.driver._extract_connection_info(farg, farg).AndReturn(fake_info) + self.driver._remove_zfcp(farg, farg, farg, farg).AndReturn(None) + self.driver._remove_zfcp_from_pool(farg, farg).AndReturn(None) + self.driver._notice_detach(farg, farg, farg, farg, farg).AndRaise( + exception.ZVMVolumeError(msg='No msg')) + self.driver._attach_device(farg, farg).AndRaise( + exception.ZVMVolumeError(msg='No msg')) + self.driver._notice_attach(farg, farg, farg, farg, farg).AndRaise( + exception.ZVMVolumeError(msg='No msg')) + self.driver._add_zfcp_to_pool(farg, farg, farg, farg).AndRaise( + exception.ZVMVolumeError(msg='No msg')) + self.driver._allocate_zfcp(farg, farg, farg, farg, farg).AndReturn( + None) + self.mox.ReplayAll() + + self.assertRaises(exception.ZVMVolumeError, + self.driver.detach_volume_inactive, + farg, fake_instance, farg, rollback) + self.assertTrue(self.driver._is_fcp_in_use(fake_instance, '1faa')) + self.mox.VerifyAll() + self.driver._instance_fcp_map = {} + + def test_detach_volume_inactive_and_detach(self): + fake_info = ('lun', 'wwpn', '1G', '1faa') + fake_instance = {'name': 'fake'} + self.driver._instance_fcp_map = {'fake': {'fcp': '1faa', 'count': 1}} + rollback = True + + self.mox.StubOutWithMock(self.driver, '_extract_connection_info') + self.mox.StubOutWithMock(self.driver, '_remove_zfcp') + self.mox.StubOutWithMock(self.driver, '_remove_zfcp_from_pool') + self.mox.StubOutWithMock(self.driver, '_notice_detach') + self.mox.StubOutWithMock(self.driver, '_detach_device') + + farg = mox.IgnoreArg() + self.driver._extract_connection_info(farg, farg).AndReturn(fake_info) + self.driver._remove_zfcp(farg, farg, farg, farg).AndReturn(None) + self.driver._remove_zfcp_from_pool(farg, farg).AndReturn(None) + self.driver._notice_detach(farg, farg, farg, farg, farg).AndReturn( + None) + self.driver._detach_device(farg, farg).AndReturn(None) + self.mox.ReplayAll() + + self.driver.detach_volume_inactive(farg, fake_instance, + farg, rollback) + self.assertFalse(self.driver._is_fcp_in_use(fake_instance, '1faa')) + self.mox.VerifyAll() + self.driver._instance_fcp_map = {} + + def test_detach_volume_inactive_no_detach(self): + fake_info = ('lun', 'wwpn', '1G', '1faa') + fake_instance = {'name': 'fake'} + self.driver._instance_fcp_map = {'fake': {'fcp': '1faa', 'count': 2}} + rollback = True + + self.mox.StubOutWithMock(self.driver, '_extract_connection_info') + self.mox.StubOutWithMock(self.driver, '_remove_zfcp') + self.mox.StubOutWithMock(self.driver, '_remove_zfcp_from_pool') + self.mox.StubOutWithMock(self.driver, '_notice_detach') + + farg = mox.IgnoreArg() + self.driver._extract_connection_info(farg, farg).AndReturn(fake_info) + self.driver._remove_zfcp(farg, farg, farg, farg).AndReturn(None) + self.driver._remove_zfcp_from_pool(farg, farg).AndReturn(None) + self.driver._notice_detach(farg, farg, farg, farg, farg).AndReturn( + None) + self.mox.ReplayAll() + + self.driver.detach_volume_inactive(farg, fake_instance, farg, rollback) + self.assertTrue(self.driver._is_fcp_in_use(fake_instance, '1faa')) + self.mox.VerifyAll() + self.driver._instance_fcp_map = {} + + def test_expand_fcp_list(self): + fcp_list = '01aF;01b9-01BB;01BA-01BC;0000;9999;AAAA;FFFF;1;a' + target_list = set(['01af', '01b9', '01ba', '01bb', '01bc', '0000', + '9999', 'aaaa', 'ffff', '0001', '000a']) + self.assertEqual(target_list, self.driver._expand_fcp_list(fcp_list)) + + def test_is_fcp_in_use_no_record(self): + self.driver._instance_fcp_map = {} + fake_instance = {'name': 'fake'} + self.assertFalse(self.driver._is_fcp_in_use(fake_instance, '1faa')) + self.mox.VerifyAll() + + def test_is_fcp_in_use_reserved(self): + self.driver._instance_fcp_map = {'fake': {'fcp': '1faa', 'count': 0}} + fake_instance = {'name': 'fake'} + self.assertFalse(self.driver._is_fcp_in_use(fake_instance, '1faa')) + self.mox.VerifyAll() + + def test_is_fcp_in_use_only_one(self): + self.driver._instance_fcp_map = {'fake': {'fcp': '1faa', 'count': 1}} + fake_instance = {'name': 'fake'} + self.assertTrue(self.driver._is_fcp_in_use(fake_instance, '1faa')) + self.mox.VerifyAll() + + def test_is_fcp_in_use_multi_volume(self): + self.driver._instance_fcp_map = {'fake': {'fcp': '1faa', 'count': 3}} + fake_instance = {'name': 'fake'} + self.assertTrue(self.driver._is_fcp_in_use(fake_instance, '1faa')) + self.mox.VerifyAll() + + +class ZVMImageOPTestCases(ZVMTestCase): + """ basic test cases of image operation""" + + def setUp(self): + super(ZVMImageOPTestCases, self).setUp() + self.imageop = imageop.ZVMImages() + self.res_data = {"data": [{"data": [["song017c: Filesystem \ + Size Used Avail Use% Mounted on\nsong017c: \ + /dev/dasda1 3.0G 1.7G 1.2G 60% /"]]}, + {"errorcode": [["0"]], "NoErrorPrefix": [["1"]], + "error": [["song017c: @@@@@@@@@@@@@@@@@@@@@@@@@@@\ + @@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@\n", "song017c: @ \ + WARNING: REMOTE HOST IDENTIFICATION HAS CHANGED!\ + @\n", "song017c: @@@@@@@@@@@@@@@@@\ + @@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@\n", + "song017c: IT IS POSSIBLE THAT SOMEONE IS \ + DOING SOMETHING NASTY!\n", + "song017c: Someone could be eavesdropping on \ + you right now (man-in-the-middle attack)!\n", + "song017c: It is also possible that a host key \ + has just been changed.\n", + "song017c: The fingerprint for the RSA key sent \ + by the remote host is\nsong017c: d9:9c:71:ed:be:8e:\ + cd:0e:b1:1a:f3:fe:a8:30:84:8b.\n", + "song017c: Please contact your administrator.\n", + "song017c: Add host key in /root/.ssh/known_hosts\ + to get rid of this message.\n", + "song017c: /root/.ssh/known_hosts:22\n", + "song017c: Password authentication is disabled \ + to avoid man-in-the-middle attacks.\n", + "song017c: Keyboard-interactive authentication \ + is disabled to avoid man-the-middle attacks.\n"]]}, + {"errorcode": ["0"]}]} + + def test_get_imgcapture_needed_in_compression_0(self): + self.mox.StubOutWithMock(zvmutils, 'xcat_request') + zvmutils.xcat_request('PUT', mox.IgnoreArg(), + mox.IgnoreArg()).AndReturn(self.res_data['data'][0]) + self.mox.ReplayAll() + size = self.imageop.get_imgcapture_needed(self.instance) + self.assertEqual(size, float(3.0) * 2) + + self.mox.VerifyAll() + + def test_get_imgcapture_needed_in_compression_6(self): + self.mox.StubOutWithMock(zvmutils, 'xcat_request') + zvmutils.xcat_request('PUT', mox.IgnoreArg(), + mox.IgnoreArg()).AndReturn(self.res_data['data'][0]) + self.mox.ReplayAll() + self.flags(zvm_image_compression_level='6') + size = self.imageop.get_imgcapture_needed(self.instance) + self.assertEqual(size, float(1.7) * 2) + + self.mox.VerifyAll() diff --git a/nova-zvm-virt-driver/nova/virt/zvm/__init__.py b/nova-zvm-virt-driver/nova/virt/zvm/__init__.py new file mode 100755 index 0000000..6b9656d --- /dev/null +++ b/nova-zvm-virt-driver/nova/virt/zvm/__init__.py @@ -0,0 +1,31 @@ +# vim: tabstop=4 shiftwidth=4 softtabstop=4 + +# Copyright 2013 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. + +"""A connection to an IBM z/VM Virtualization system. + +Generally, OpenStack z/VM virt driver will call xCat REST API to operate +to z/VM hypervisors.xCat has a control point(a virtual machine) in z/VM +system, which enables xCat management node to control the z/VM system. +OpenStack z/VM driver will communicate with xCat management node through +xCat REST API. Thus OpenStack can operate to z/VM system indirectly. + +""" + + +from nova.virt.zvm import driver + + +ZVMDriver = driver.ZVMDriver diff --git a/nova-zvm-virt-driver/nova/virt/zvm/configdrive.py b/nova-zvm-virt-driver/nova/virt/zvm/configdrive.py new file mode 100755 index 0000000..494996b --- /dev/null +++ b/nova-zvm-virt-driver/nova/virt/zvm/configdrive.py @@ -0,0 +1,65 @@ +# vim: tabstop=4 shiftwidth=4 softtabstop=4 + +# Copyright 2013 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 os +import tarfile + +from oslo.config import cfg + +from nova import exception +from nova import utils +from nova.virt import configdrive + + +CONF = cfg.CONF + + +class ZVMConfigDriveBuilder(configdrive.ConfigDriveBuilder): + """Enable ConfigDrive to make tgz package.""" + + def __init__(self, instance_md): + super(ZVMConfigDriveBuilder, self).__init__(instance_md) + + def make_drive(self, path): + """Make the config drive. + + :param path: the path to place the config drive image at + :raises ProcessExecuteError if a helper process has failed. + + """ + if CONF.config_drive_format == 'tgz': + self._make_tgz(path) + else: + raise exception.ConfigDriveUnknownFormat( + format=CONF.config_drive_format) + + def _make_tgz(self, path): + try: + olddir = os.getcwd() + except OSError: + olddir = CONF.state_path + + with utils.tempdir() as tmpdir: + self._write_md_files(tmpdir) + tar = tarfile.open(path, "w:gz") + os.chdir(tmpdir) + tar.add("openstack") + tar.add("ec2") + try: + os.chdir(olddir) + except Exception: + pass + tar.close() diff --git a/nova-zvm-virt-driver/nova/virt/zvm/const.py b/nova-zvm-virt-driver/nova/virt/zvm/const.py new file mode 100755 index 0000000..707d41f --- /dev/null +++ b/nova-zvm-virt-driver/nova/virt/zvm/const.py @@ -0,0 +1,68 @@ +# vim: tabstop=4 shiftwidth=4 softtabstop=4 + +# Copyright 2013 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.compute import power_state + + +HYPERVISOR_TYPE = 'zvm' +ARCHITECTURE = 's390x' +ALLOWED_VM_TYPE = 'zLinux' +XCAT_MGT = 'zvm' + +XCAT_RINV_HOST_KEYWORDS = { + "zvm_host": "z/VM Host:", + "zhcp": "zHCP:", + "cec_vendor": "CEC Vendor:", + "cec_model": "CEC Model:", + "hypervisor_os": "Hypervisor OS:", + "hypervisor_name": "Hypervisor Name:", + "architecture": "Architecture:", + "lpar_cpu_total": "LPAR CPU Total:", + "lpar_cpu_used": "LPAR CPU Used:", + "lpar_memory_total": "LPAR Memory Total:", + "lpar_memory_used": "LPAR Memory Used:", + "lpar_memory_offline": "LPAR Memory Offline:", + "ipl_time": "IPL Time:", + } + +XCAT_DISKPOOL_KEYWORDS = { + "disk_total": "Total:", + "disk_used": "Used:", + "disk_available": "Free:", + } + +XCAT_RESPONSE_KEYS = ('info', 'data', 'node', 'errorcode', 'error') + +ZVM_POWER_STAT = { + 'on': power_state.RUNNING, + 'off': power_state.SHUTDOWN, + } + +ZVM_DEFAULT_ROOT_DISK = "dasda" +ZVM_DEFAULT_SECOND_DISK = "dasdb" +ZVM_DEFAULT_ROOT_VOLUME = "sda" +ZVM_DEFAULT_SECOND_VOLUME = "sdb" +ZVM_DEFAULT_THIRD_VOLUME = "sdc" +ZVM_DEFAULT_LAST_VOLUME = "sdz" + +DEFAULT_EPH_DISK_FMT = "ext3" + +ZVM_DEFAULT_FCP_ID = 'auto' + +ZVM_DEFAULT_NIC_VDEV = '1000' + +ZVM_IMAGE_SIZE_MAX = 10 diff --git a/nova-zvm-virt-driver/nova/virt/zvm/driver.py b/nova-zvm-virt-driver/nova/virt/zvm/driver.py new file mode 100755 index 0000000..9b066f7 --- /dev/null +++ b/nova-zvm-virt-driver/nova/virt/zvm/driver.py @@ -0,0 +1,1889 @@ +# vim: tabstop=4 shiftwidth=4 softtabstop=4 + +# Copyright 2013 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 datetime +import os +import time +import uuid + +from oslo.config import cfg + +from nova.api.metadata import base as instance_metadata +from nova.compute import power_state +from nova.compute import task_states +from nova import exception as nova_exception +from nova.image import glance +from nova.openstack.common import excutils +from nova.openstack.common.gettextutils import _ +from nova.openstack.common import jsonutils +from nova.openstack.common import log as logging +from nova.openstack.common import loopingcall +from nova.openstack.common import units +from nova.openstack.common import timeutils +from nova import utils +from nova.virt import configdrive +from nova.virt import driver +from nova.virt.zvm import configdrive as zvmconfigdrive +from nova.virt.zvm import const +from nova.virt.zvm import exception +from nova.virt.zvm import imageop +from nova.virt.zvm import instance as zvminstance +from nova.virt.zvm import networkop +from nova.virt.zvm import utils as zvmutils +from nova.virt.zvm import volumeop + + +LOG = logging.getLogger(__name__) + +zvm_opts = [ + 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, + help='Password of the xCAT user'), + cfg.StrOpt('zvm_diskpool', + default=None, + help='z/VM disk pool for ephemeral disks'), + cfg.StrOpt('zvm_host', + default=None, + help='z/VM host that managed by xCAT MN.'), + cfg.StrOpt('zvm_xcat_group', + default='all', + help='xCAT group for OpenStack'), + cfg.StrOpt('zvm_scsi_pool', + default='xcatzfcp', + help='Default zfcp scsi disk pool'), + cfg.StrOpt('zvm_fcp_list', + default=None, + help='Configured fcp list can be used'), + cfg.StrOpt('zvm_zhcp_fcp_list', + default=None, + help='Configured fcp list dedicated to hcp'), + cfg.StrOpt('zvm_diskpool_type', + default='ECKD', + help='Default disk type for root disk, can be ECKD/FBA'), + cfg.BoolOpt('zvm_config_drive_inject_password', + default=False, + help='Sets the admin password in the config drive'), + cfg.StrOpt('zvm_vmrelocate_force', + default=None, + help='force can be: (ARCHITECTURE) attempt relocation even ' + 'though hardware architecture facilities or CP features ' + 'are not available on destination system, ' + '(DOMAIN) attempt relocation even though VM would be ' + 'moved outside of its domain, ' + 'or (STORAGE) relocation should proceed even if CP ' + 'determines that there are insufficient storage ' + 'resources on destination system.'), + cfg.StrOpt('zvm_vmrelocate_immediate', + default='yes', + help='immediate can be: (YES) VMRELOCATE command will do ' + 'one early pass through virtual machine storage and ' + 'then go directly to the quiesce stage, ' + 'or (NO) specifies immediate processing.'), + cfg.StrOpt('zvm_vmrelocate_max_total', + default='nolimit', + help='Maximum wait time(seconds) for relocation to complete'), + cfg.StrOpt('zvm_vmrelocate_max_quiesce', + default='nolimit', + help='Maximum quiesce time(seconds) a VM may be stopped ' + 'during a relocation attempt'), + cfg.IntOpt('zvm_reachable_timeout', + default=300, + help='Timeout(seconds) when start an instance.'), + cfg.IntOpt('zvm_xcat_connection_timeout', + default=3600, + help='xCAT connection read timeout(seconds)'), + cfg.IntOpt('zvm_console_log_size', + default=100, + help='Max console log size(kilobyte) get from xCAT'), + ] + +zvm_user_opts = [ + cfg.StrOpt('zvm_user_profile', + default=None, + help='User profile for creating a z/VM userid'), + cfg.StrOpt('zvm_user_default_password', + default='dfltpass', + help='Default password for a new created z/VM user'), + cfg.StrOpt('zvm_user_default_privilege', + default='g', + help='Default privilege level for a new created z/VM user'), + cfg.StrOpt('zvm_user_root_vdev', + default='0100', + help='Virtual device number for ephemeral root disk'), + cfg.StrOpt('zvm_user_adde_vdev', + default='0101', + help='Virtual device number for additional ephemeral disk'), + cfg.StrOpt('zvm_user_volume_vdev', + default='0200', + help='Virtual device number for persistent volume, ' + 'if there are more then one volumes, will use next vdev'), + ] + +zvm_image_opts = [ + cfg.StrOpt('zvm_image_tmp_path', + default='/var/lib/nova/images', + help='The path to store the z/VM image files'), + cfg.StrOpt('zvm_default_ephemeral_mntdir', + default='/mnt/ephemeral', + help='The path to which the ephemeral disk be mounted'), + cfg.StrOpt('zvm_image_default_password', + default='rootpass', + help='Default os root password for a new created vm'), + cfg.IntOpt('xcat_image_clean_period', + default=30, + help='The period(days) to clean up an image that not be used ' + 'for deploy in one xCAT MN within the defined time'), + cfg.IntOpt('xcat_free_space_threshold', + default=50, + help='The threshold for xCAT free space, if snapshot or spawn ' + 'check xCAT free space not enough for its image ' + 'operations, it will prune image to meet the threshold'), + cfg.StrOpt('zvm_xcat_master', + default=None, + help='The xCAT MM node name'), + cfg.StrOpt('zvm_image_compression_level', + default=None, + help='The level of gzip compression used when capturing disk'), + ] + +CONF = cfg.CONF +CONF.register_opts(zvm_opts) +CONF.register_opts(zvm_user_opts) +CONF.register_opts(zvm_image_opts) +CONF.import_opt('host', 'nova.netconf') +CONF.import_opt('my_ip', 'nova.netconf') +CONF.import_opt('default_ephemeral_format', 'nova.virt.driver') + +ZVMInstance = zvminstance.ZVMInstance + + +class ZVMDriver(driver.ComputeDriver): + """z/VM implementation of ComputeDriver.""" + + capabilities = { + "has_imagecache": True, + } + + def __init__(self, virtapi): + super(ZVMDriver, self).__init__(virtapi) + self._xcat_url = zvmutils.XCATUrl() + + self._host_stats = [] + try: + self._host_stats = self.get_host_stats(refresh=True) + self._networkop = networkop.NetworkOperator() + except Exception as e: + # Ignore any exceptions and log as warning + LOG.warn(_("Exception raised while initializing z/VM driver: %s") + % e) + + self._zvm_images = imageop.ZVMImages() + self._pathutils = zvmutils.PathUtils() + self._networkutils = zvmutils.NetworkUtils() + self._volumeop = volumeop.VolumeOperator() + + def init_host(self, host): + """Initialize anything that is necessary for the driver to function, + including catching up with currently running VM's on the given host. + """ + try: + self._volumeop.init_host(self._host_stats) + except Exception as e: + LOG.warn(_("Exception raised while initializing z/VM driver: %s") + % e) + + def get_info(self, instance): + """Get the current status of an instance, by name (not ID!) + + Returns a dict containing: + :state: the running state, one of the power_state codes + :max_mem: (int) the maximum memory in KBytes allowed + :mem: (int) the memory in KBytes used by the domain + :num_cpu: (int) the number of virtual CPUs for the domain + :cpu_time: (int) the CPU time used in nanoseconds + + """ + inst_name = instance['name'] + zvm_inst = ZVMInstance(instance) + + try: + return zvm_inst.get_info() + except exception.ZVMXCATRequestFailed as err: + emsg = err.format_message() + if (emsg.__contains__("Invalid nodes and/or groups") and + emsg.__contains__("Forbidden")): + LOG.warn(_("z/VM instance %s does not exist") % inst_name, + instance=instance) + raise nova_exception.InstanceNotFound(instance_id=inst_name) + else: + raise err + + def list_instances(self): + """Return the names of all the instances known to the virtualization + layer, as a list. + """ + zvm_host = CONF.zvm_host + hcp_base = self._get_hcp_info()['hostname'] + + url = self._xcat_url.tabdump("/zvm") + res_dict = zvmutils.xcat_request("GET", url) + + instances = [] + + with zvmutils.expect_invalid_xcat_resp_data(): + data_entries = res_dict['data'][0][1:] + for data in data_entries: + l = data.split(",") + node, hcp = l[0].strip("\""), l[1].strip("\"") + hcp_short = hcp_base.partition('.')[0] + + # zvm host and zhcp are not included in the list + if (hcp.upper() == hcp_base.upper() and + node.upper() not in (zvm_host.upper(), + hcp_short.upper(), CONF.zvm_xcat_master.upper())): + instances.append(node) + + return instances + + def instance_exists(self, instance_name): + """Overwrite this to using instance name as input parameter.""" + return instance_name in self.list_instances() + + def spawn(self, context, instance, image_meta, injected_files, + admin_password, network_info=None, block_device_info=None): + """Create a new instance/VM/domain on the virtualization platform. + + Once this successfully completes, the instance should be + running (power_state.RUNNING). + + If this fails, any partial instance should be completely + cleaned up, and the virtualization platform should be in the state + that it was before this call began. + + :param context: security context + :param instance: Instance object as returned by DB layer. + This function should use the data there to guide + the creation of the new instance. + :param image_meta: image object returned by nova.image.glance that + defines the image from which to boot this instance + :param injected_files: User files to inject into instance. + :param admin_password: Administrator password to set in instance. + :param network_info: + :py:meth:`~nova.network.manager.NetworkManager.get_instance_nw_info` + :param block_device_info: Information about block devices to be + attached to the instance. + + """ + boot_from_volume = zvmutils.volume_in_mapping( + const.ZVM_DEFAULT_ROOT_VOLUME, + block_device_info) + if boot_from_volume: + msg = _("Not support boot from volume.") + raise exception.ZVMDriverError(msg=msg) + + if not network_info: + msg = _("Not support boot without a NIC. " + "A NIC connected to xCAT management network is required.") + raise exception.ZVMDriverError(msg=msg) + + # Ensure the used image is a valid zVM image + self._zvm_images.zimage_check(image_meta) + + compute_node = CONF.zvm_host + zhcp = self._get_hcp_info()['hostname'] + + zvm_inst = ZVMInstance(instance) + instance_path = self._pathutils.get_instance_path(compute_node, + zvm_inst._name) + # Create network configuration files + LOG.debug(_('Creating network configuration files ' + 'for instance: %s') % zvm_inst._name, instance=instance) + base_nic_vdev = const.ZVM_DEFAULT_NIC_VDEV + os_version = image_meta['properties']['os_version'] + # Check the OS type + os_type, version = zvmutils.parse_os_version(os_version) + files_and_cmds = self._networkutils.create_network_configuration_files( + instance_path, network_info, base_nic_vdev, + os_type) + (net_conf_files, net_conf_cmds) = files_and_cmds + # Add network configure files to inject_files + if len(net_conf_files) > 0: + injected_files.extend(net_conf_files) + + # Create configure drive + if not CONF.zvm_config_drive_inject_password: + admin_password = CONF.zvm_image_default_password + transportfiles = None + if configdrive.required_by(instance): + transportfiles = self._create_config_drive(instance_path, + instance, injected_files, admin_password, net_conf_cmds) + + LOG.info(_("The instance %(name)s is spawning at %(node)s") % + {'name': zvm_inst._name, 'node': compute_node}, + instance=instance) + + spawn_start = time.time() + + try: + # Create xCAT node for the instance and preset network + zvm_inst.create_xcat_node(zhcp) + self._preset_instance_network(zvm_inst._name, network_info) + + tmp_file_fn = None + bundle_file_path = None + if 'root_disk_units' not in image_meta['properties']: + (tmp_file_fn, image_file_path, bundle_file_path) = \ + self._import_image_to_nova(context, instance, image_meta) + image_meta = self._zvm_images.set_image_root_disk_units( + context, image_meta, image_file_path) + image_in_xcat = self._zvm_images.image_exist_xcat( + instance['image_ref']) + if not image_in_xcat: + self._import_image_to_xcat(context, instance, image_meta, + tmp_file_fn) + elif bundle_file_path is not None: + self._pathutils.clean_temp_folder(bundle_file_path) + + # Create z/VM userid and update node info for instance + zvm_inst.create_userid(block_device_info, image_meta) + zvm_inst.update_node_info(image_meta) + + # Create nic for z/VM instance + nic_vdev = base_nic_vdev + zhcpnode = self._get_hcp_info()['nodename'] + for vif in network_info: + LOG.debug(_('Create nic for instance: %(inst)s, MAC: ' + '%(mac)s Network: %(network)s Vdev: %(vdev)s') % + {'inst': zvm_inst._name, 'mac': vif['address'], + 'network': vif['network']['label'], + 'vdev': nic_vdev}, instance=instance) + self._networkop.create_nic(zhcpnode, zvm_inst._name, + vif['id'], + vif['address'], + nic_vdev) + nic_vdev = str(hex(int(nic_vdev, 16) + 3))[2:] + + # Call nodeset restapi to deploy image on node + deploy_image_name = self._zvm_images.get_imgname_xcat( + instance['image_ref']) + zvm_inst.deploy_node(deploy_image_name, transportfiles) + + # Change vm's admin password during spawn + zvmutils.punch_adminpass_file(instance_path, zvm_inst._name, + admin_password) + + # Unlock the instance + zvmutils.punch_xcat_auth_file(instance_path, zvm_inst._name) + + # punch ephemeral disk info to the instance + if instance['ephemeral_gb'] != 0: + eph_disks = block_device_info.get('ephemerals', []) + if eph_disks == []: + zvmutils.punch_eph_info_file(instance_path, zvm_inst._name) + else: + for idx, eph in enumerate(eph_disks): + vdev = zvmutils.generate_eph_vdev(idx) + fmt = eph.get('guest_format') + mount_dir = ''.join([CONF.zvm_default_ephemeral_mntdir, + str(idx)]) + zvmutils.punch_eph_info_file(instance_path, + zvm_inst._name, vdev, fmt, mount_dir) + + # Wait until network configuration finish + self._wait_for_addnic(zvm_inst._name) + if not self._is_nic_granted(zvm_inst._name): + msg = _("Failed to bound vswitch") + LOG.error(msg, instance=instance) + raise exception.ZVMNetworkError(msg=msg) + + # Attach persistent volume + bdm = driver.block_device_info_get_mapping(block_device_info) + self._attach_volume_to_instance(context, instance, bdm) + + # Power on the instance, then put MN's public key into instance + zvm_inst.power_on() + + # Update the root device name in instance table + root_device_name = ''.join(['/dev/', + const.ZVM_DEFAULT_ROOT_VOLUME]) + instance.root_device_name = root_device_name + instance.save() + + spawn_time = time.time() - spawn_start + LOG.info(_("Instance spawned succeeded in %s seconds") % + spawn_time, instance=instance) + except (exception.ZVMXCATCreateNodeFailed, + exception.ZVMImageError): + with excutils.save_and_reraise_exception(): + zvm_inst.delete_xcat_node() + except (exception.ZVMXCATCreateUserIdFailed, + exception.ZVMNetworkError, + exception.ZVMVolumeError, + exception.ZVMXCATUpdateNodeFailed, + exception.ZVMXCATDeployNodeFailed): + with excutils.save_and_reraise_exception(): + self.destroy(context, instance, network_info, + block_device_info) + except Exception as err: + # Just a error log then re-raise + with excutils.save_and_reraise_exception(): + LOG.error(_("Deploy image to instance %(instance)s " + "failed with reason: %(err)s") % + {'instance': zvm_inst._name, 'err': err}, + instance=instance) + finally: + self._pathutils.clean_temp_folder(instance_path) + + # Update image last deploy date in xCAT osimage table + self._zvm_images.update_last_use_date(deploy_image_name) + + def _create_config_drive(self, instance_path, instance, injected_files, + admin_password, commands): + if CONF.config_drive_format != 'tgz': + msg = _("Invalid config drive format %s" % + CONF.config_drive_format) + raise exception.ZVMConfigDriveError(msg=msg) + + LOG.debug(_('Using config drive'), instance=instance) + + extra_md = {} + if CONF.zvm_config_drive_inject_password: + extra_md['admin_pass'] = admin_password + + # Append commands which are used to configure network devices + udev_settle_sles = '\n'.join(('cio_ignore -R', + 'udevadm trigger', + 'udevadm settle', 'sleep 2', + 'service network restart', + 'cio_ignore -u')) + + udev_settle_rhel = '\n'.join(('cio_ignore -R', + 'znetconf -R <> /root/.ssh/authorized_keys' % + dest_diff_mn_key) + zvmutils.xdsh(inst_name, auth_command) + + try: + self._vmrelocate(dest_host, inst_name, 'move') + except nova_exception.MigrationError as err: + LOG.error(_("Live-migration failed: %s") % err, + instance=instance_ref) + with excutils.save_and_reraise_exception(): + recover_method(ctxt, instance_ref, dest, + block_migration, migrate_data) + + if not same_mn: + # Delete node definition at source xCAT MN + zvm_inst = ZVMInstance(instance_ref) + self._networkop.clean_mac_switch_host(zvm_inst._name) + zvm_inst.delete_xcat_node() + + post_method(ctxt, instance_ref, dest, + block_migration, migrate_data) + + def post_live_migration_at_destination(self, ctxt, instance_ref, + network_info, + block_migration=False, + block_device_info=None): + """Post operation of live migration at destination host. + + :param ctxt: security context + :param instance_ref: instance object that is migrated + :param network_info: instance network information + :param block_migration: if true, post operation of block_migration. + + """ + inst_name = instance_ref['name'] + nic_vdev = const.ZVM_DEFAULT_NIC_VDEV + zhcp = self._get_hcp_info()['hostname'] + + for vif in network_info: + LOG.debug(_('Create nic for instance: %(inst)s, MAC: ' + '%(mac)s Network: %(network)s Vdev: %(vdev)s') % + {'inst': inst_name, 'mac': vif['address'], + 'network': vif['network']['label'], 'vdev': nic_vdev}, + instance=instance_ref) + self._networkop.add_xcat_mac(inst_name, nic_vdev, + vif['address'], zhcp) + self._networkop.add_xcat_switch(inst_name, vif['id'], + nic_vdev, zhcp) + nic_vdev = str(hex(int(nic_vdev, 16) + 3))[2:] + + def unfilter_instance(self, instance, network_info): + """Stop filtering instance.""" + # Not supported for now + return + + def _vmrelocate(self, dest_host, instance_name, action): + """Perform live-migration.""" + body = ['destination=%s' % dest_host, + 'action=%s' % action, + 'immediate=%s' % CONF.zvm_vmrelocate_immediate, + 'max_total=%s' % CONF.zvm_vmrelocate_max_total, + 'max_quiesce=%s' % CONF.zvm_vmrelocate_max_quiesce] + if CONF.zvm_vmrelocate_force is not None: + body.append('force=%s' % CONF.zvm_vmrelocate_force) + + url = self._xcat_url.rmigrate('/' + instance_name) + try: + res = zvmutils.xcat_request("PUT", url, body) + except (exception.ZVMXCATRequestFailed, + exception.ZVMInvalidXCATResponseDataError, + exception.ZVMXCATInternalError) as err: + raise nova_exception.MigrationError(reason=err) + + res_info = res['info'] + if not (len(res_info) > 0 and + len(res_info[0]) > 0 and + res_info[-1][0].__contains__("Done")): + msg = _("Live-migration failed: %s") % str(res_info) + LOG.error(msg) + raise nova_exception.MigrationError(reason=msg) + + def reset_network(self, instance): + """reset networking for specified instance.""" + # TODO(rui): to implement this later. + pass + + def inject_network_info(self, instance, nw_info): + """inject network info for specified instance.""" + # TODO(rui): to implement this later. + pass + + def plug_vifs(self, instance, network_info): + """Plug VIFs into networks.""" + # TODO(rui): to implement this later. + pass + + def unplug_vifs(self, instance, network_info): + """Unplug VIFs from networks.""" + # TODO(rui): to implement this later + pass + + def ensure_filtering_rules_for_instance(self, instance_ref, network_info): + # It enforces security groups on host initialization and live + # migration. In z/VM we do not assume instances running upon host + # initialization + return + + def update_host_status(self): + """Refresh host stats. One compute service entry possibly + manages several hypervisors, so will return a list of host + status information. + """ + LOG.debug(_("Updating host status for %s") % CONF.zvm_host) + + caps = [] + host = CONF.zvm_host + + info = self._get_host_inventory_info(host) + + data = {'host': CONF.host, + 'allowed_vm_type': const.ALLOWED_VM_TYPE} + data['vcpus'] = info['vcpus'] + data['vcpus_used'] = info['vcpus_used'] + data['cpu_info'] = info['cpu_info'] + data['disk_total'] = info['disk_total'] + data['disk_used'] = info['disk_used'] + data['disk_available'] = info['disk_available'] + data['host_memory_total'] = info['memory_mb'] + data['host_memory_free'] = (info['memory_mb'] - + info['memory_mb_used']) + data['hypervisor_type'] = info['hypervisor_type'] + data['hypervisor_version'] = info['hypervisor_version'] + data['hypervisor_hostname'] = info['hypervisor_hostname'] + data['supported_instances'] = [(const.ARCHITECTURE, + const.HYPERVISOR_TYPE)] + data['zhcp'] = self._get_hcp_info(info['zhcp']) + data['ipl_time'] = info['ipl_time'] + + caps.append(data) + + return caps + + def _get_hcp_info(self, hcp_hostname=None): + if self._host_stats != []: + return self._host_stats[0]['zhcp'] + else: + if hcp_hostname is not None: + hcp_node = hcp_hostname.partition('.')[0] + return {'hostname': hcp_hostname, + 'nodename': hcp_node, + 'userid': zvmutils.get_userid(hcp_node)} + else: + self.get_host_stats(refresh=True) + return self._host_stats[0]['zhcp'] + + def get_host_stats(self, refresh=False): + """Return currently known host stats.""" + if refresh: + self._host_stats = self.update_host_status() + return self._host_stats + + def get_volume_connector(self, instance): + """Get connector information for the instance for attaching to volumes. + + Connector information is a dictionary representing the ip of the + machine that will be making the connection, the name of the iscsi + initiator and the hostname of the machine as follows:: + + { + 'zvm_fcp': fcp + 'wwpns': [wwpn] + 'host': host + } + """ + LOG.debug(_("Getting volume connector")) + + res = self._volumeop.get_volume_connector(instance) + + return res + + def _get_host_inventory_info(self, host): + url = self._xcat_url.rinv('/' + host) + inv_info_raw = zvmutils.xcat_request("GET", url)['info'][0] + inv_keys = const.XCAT_RINV_HOST_KEYWORDS + inv_info = zvmutils.translate_xcat_resp(inv_info_raw[0], inv_keys) + dp_info = self._get_diskpool_info(host) + + host_info = {} + + with zvmutils.expect_invalid_xcat_resp_data(): + host_info['vcpus'] = int(inv_info['lpar_cpu_total']) + host_info['vcpus_used'] = int(inv_info['lpar_cpu_used']) + host_info['cpu_info'] = {} + host_info['cpu_info'] = {'architecture': const.ARCHITECTURE, + 'vm_mode': inv_info['cec_model'], } + host_info['disk_total'] = dp_info['disk_total'] + host_info['disk_used'] = dp_info['disk_used'] + host_info['disk_available'] = dp_info['disk_available'] + mem_mb = zvmutils.convert_to_mb(inv_info['lpar_memory_total']) + host_info['memory_mb'] = mem_mb + mem_mb_used = zvmutils.convert_to_mb(inv_info['lpar_memory_used']) + host_info['memory_mb_used'] = mem_mb_used + host_info['hypervisor_type'] = const.HYPERVISOR_TYPE + verl = inv_info['hypervisor_os'].split()[1].split('.') + version = int(''.join(verl)) + host_info['hypervisor_version'] = version + host_info['hypervisor_hostname'] = inv_info['hypervisor_name'] + host_info['zhcp'] = inv_info['zhcp'] + host_info['ipl_time'] = inv_info['ipl_time'] + + return host_info + + def _get_diskpool_info(self, host): + addp = '&field=--diskpoolspace&field=' + CONF.zvm_diskpool + url = self._xcat_url.rinv('/' + host, addp) + res_dict = zvmutils.xcat_request("GET", url) + + dp_info_raw = res_dict['info'][0] + dp_keys = const.XCAT_DISKPOOL_KEYWORDS + dp_info = zvmutils.translate_xcat_resp(dp_info_raw[0], dp_keys) + + with zvmutils.expect_invalid_xcat_resp_data(): + for k in dp_info.keys(): + s = dp_info[k].strip().upper() + if s.endswith('G'): + sl = s[:-1].split('.') + n1, n2 = int(sl[0]), int(sl[1]) + if n2 >= 5: + n1 += 1 + dp_info[k] = n1 + elif s.endswith('M'): + n_mb = int(s[:-1]) + n_gb, n_ad = n_mb / 1024, n_mb % 1024 + if n_ad >= 512: + n_gb += 1 + dp_info[k] = n_gb + else: + exp = "ending with a 'G' or 'M'" + errmsg = _("Invalid diskpool size format: %(invalid)s; " + "Expected: %(exp)s") % {'invalid': s, 'exp': exp} + LOG.error(errmsg) + raise exception.ZVMDriverError(msg=errmsg) + + return dp_info + + def migrate_disk_and_power_off(self, context, instance, dest, + instance_type, network_info, + block_device_info=None, + timeout=0, retry_interval=0): + """ + Transfers the disk of a running instance in multiple phases, turning + off the instance before the end. + """ + is_volume_base = zvmutils.volume_in_mapping( + const.ZVM_DEFAULT_ROOT_VOLUME, block_device_info) + if is_volume_base: + msg = _("Not support boot from volume.") + LOG.error(msg, instance=instance) + raise nova_exception.MigrationError(reason=msg) + + # Make sure the instance's power_state is running and unpaused before + # doing a capture. + state = instance['power_state'] + if (state == power_state.NOSTATE or state == power_state.CRASHED or + state == power_state.SUSPENDED): + raise nova_exception.InstanceNotReady(instance_id=instance['name']) + elif state == power_state.SHUTDOWN: + self.power_on({}, instance, []) + elif state == power_state.PAUSED: + self.unpause(instance) + + inst_name = instance['name'] + LOG.debug(_("Starting to migrate instance %(name)s to %(dest)s") % + {'name': inst_name, 'dest': dest}, instance=instance) + + disk_owner = zvmutils.get_userid(inst_name) + + sys_meta = instance['system_metadata'] + new_root_disk_size = int(sys_meta['new_instance_type_root_gb']) + new_eph_disk_size = int(sys_meta['new_instance_type_ephemeral_gb']) + old_root_disk_size = int(sys_meta['instance_type_root_gb']) + old_eph_disk_size = int(sys_meta['instance_type_ephemeral_gb']) + + if (new_root_disk_size < old_root_disk_size or + new_eph_disk_size < old_eph_disk_size): + err = _("Not support shrink disk") + LOG.error(err, instance=instance) + raise nova_exception.MigrationError(reason=err) + + eph_disk_info = self._get_eph_disk_info(inst_name) + + bdm = driver.block_device_info_get_mapping(block_device_info) + self._detach_volume_from_instance(instance, bdm) + + image_bundle, image_name_xcat = self._capture_disk_for_instance( + context, instance) + + source_image = "".join([zvmutils.get_host(), ":", image_bundle]) + disk_info = { + 'disk_type': CONF.zvm_diskpool_type, + 'disk_source_mn': CONF.zvm_xcat_server, + 'disk_source_image': source_image, + 'disk_image_name': image_name_xcat, + 'disk_owner': disk_owner, + 'disk_eph_size_old': old_eph_disk_size, + 'disk_eph_size_new': new_eph_disk_size, + 'eph_disk_info': eph_disk_info + } + + return jsonutils.dumps(disk_info) + + def _get_eph_disk_info(self, inst_name): + user_dict = self._get_user_directory(inst_name) + exl = ''.join(['MDISK ', CONF.zvm_user_root_vdev]) + eph_disks = [mdisk for mdisk in user_dict + if (mdisk.__contains__('MDISK ') and + not mdisk.__contains__(exl))] + + eph_disk_info = [] + with zvmutils.expect_invalid_xcat_resp_data(): + for eph in eph_disks: + eph_l = eph.rpartition(" MDISK ")[2].split(' ') + eph_disk_info.append({'vdev': eph_l[0], + 'size': eph_l[3], + 'guest_format': None, + 'size_in_units': True, + 'device_name': eph_l[0]}) + + return eph_disk_info + + def _detach_volume_from_instance(self, instance, block_device_mapping): + for bd in block_device_mapping: + connection_info = bd['connection_info'] + mountpoint = bd['mount_device'] + if mountpoint: + mountpoint = self._format_mountpoint(mountpoint) + + if self.instance_exists(instance['name']): + zvm_inst = ZVMInstance(instance) + is_active = zvm_inst.is_reachable() + try: + zvm_inst.detach_volume(self._volumeop, connection_info, + instance, mountpoint, is_active, + rollback=False) + except exception.ZVMVolumeError: + LOG.warn(_("Failed to detach volume from %s") % + instance['name'], instance=instance) + + def _capture_disk_for_instance(self, context, instance): + """Capture disk.""" + zvm_inst = ZVMInstance(instance) + image_name = ''.join('rsz' + instance['name']) + image_uuid = str(uuid.uuid4()) + image_href = image_uuid.replace('-', '_') + + # Capture + orig_provmethod = zvm_inst.get_provmethod() + if orig_provmethod != 'sysclone': + zvm_inst.update_node_provmethod('sysclone') + image_name_xcat = self._zvm_images.create_zvm_image(instance, + image_name, + image_href) + if orig_provmethod != 'sysclone': + zvm_inst.update_node_provmethod(orig_provmethod) + + self._zvm_images.update_last_use_date(image_name_xcat) + + # Export + snapshot_time_path = self._zvm_images.get_snapshot_time_path() + + try: + image_bundle = self._zvm_images.get_image_from_xcat( + image_name_xcat, + image_name_xcat, + snapshot_time_path) + except exception.ZVMImageError: + with excutils.save_and_reraise_exception(): + self._zvm_images.clean_up_snapshot_time_path( + snapshot_time_path) + + return image_bundle, image_name_xcat + + @contextlib.contextmanager + def cleanup_xcat_image_for_migration(self, image_name_xcat): + """Cleanup xcat image that imported by migrate_disk_and_power_off.""" + try: + yield + except (nova_exception.MigrationError, + exception.ZVMBaseException): + LOG.debug(_("Cleanup image from xCAT image repository")) + with excutils.save_and_reraise_exception(): + self._zvm_images.delete_image_from_xcat(image_name_xcat) + + def finish_migration(self, context, migration, instance, disk_info, + network_info, image_meta, resize_instance, + block_device_info=None, power_on=True): + """Completes a resize, turning on the migrated instance + + :param network_info: + :py:meth:`~nova.network.manager.NetworkManager.get_instance_nw_info` + :param image_meta: image object returned by nova.image.glance that + defines the image from which this instance + was created + """ + disk_info = jsonutils.loads(disk_info) + + source_xcat_mn = disk_info['disk_source_mn'].encode('gbk') + source_image = disk_info['disk_source_image'].encode('gbk') + image_name_xcat = disk_info['disk_image_name'].encode('gbk') + disk_type = disk_info['disk_type'].encode('gbk') + disk_eph_size_old = disk_info['disk_eph_size_old'] + disk_eph_size_new = disk_info['disk_eph_size_new'] + eph_disk_info = disk_info['eph_disk_info'] + + block_device_info = block_device_info or {} + block_device_info['ephemerals'] = [eph for eph in eph_disk_info + if eph['vdev'] != CONF.zvm_user_adde_vdev] + + old_userid = disk_info['disk_owner'].encode('gbk') + new_userid = None + if old_userid.startswith("rsz"): + new_userid = instance['name'] + else: + new_userid = ''.join(("rsz", old_userid[3:])) + + same_xcat_mn = source_xcat_mn == CONF.zvm_xcat_server + if disk_type != CONF.zvm_diskpool_type: + if same_xcat_mn: + self._zvm_images.delete_image_from_xcat(image_name_xcat) + msg = _("Can not migration between different disk type") + LOG.error(msg, instance=instance) + raise nova_exception.MigrationError(reason=msg) + + profile = image_name_xcat.split('-')[3] + source_host, t, image_bundle = source_image.partition(":") + source_ip = source_host.rpartition("@")[2] + source_image_time_path = image_bundle.rpartition('/')[0] + local_ip = self.get_host_ip_addr() + + same_os = local_ip == source_ip + + zhcp = self._get_hcp_info()['hostname'] + + new_inst = ZVMInstance(instance) + instance_path = self._pathutils.get_instance_path( + CONF.zvm_host, new_inst._name) + if same_xcat_mn: + # Same xCAT MN + # cleanup networking, will re-configure later + for vif in network_info: + self._networkop.clean_mac_switch_host(new_inst._name) + + # cleanup image bundle from source compute node + if not same_os: + utils.execute('ssh', source_host, 'rm', '-rf', + source_image_time_path) + else: + self._pathutils.clean_temp_folder(source_image_time_path) + + # Create a xCAT node poin + old_instance = self._copy_instance(instance) + old_instance['name'] = ''.join(('rsz', instance['name'])) + old_inst = ZVMInstance(old_instance) + + with self.cleanup_xcat_image_for_migration(image_name_xcat): + old_inst.copy_xcat_node(new_inst._name) + try: + new_inst.update_node_def(zhcp, new_userid) + except exception.ZVMBaseException: + with excutils.save_and_reraise_exception(): + old_inst.delete_xcat_node() + else: + # Different xCAT MN + new_inst.create_xcat_node(zhcp) + if new_userid != new_inst._name: + try: + new_inst.update_node_def(zhcp, new_userid) + except exception.ZVMBaseException: + with excutils.save_and_reraise_exception(): + old_inst.delete_xcat_node() + + if not same_os: + snapshot_time_path = self._pathutils.get_snapshot_time_path() + dest_image_path = os.path.join(snapshot_time_path, + image_name_xcat + '.tgz') + utils.execute('scp', source_image, snapshot_time_path) + utils.execute('ssh', source_host, + 'rm', '-rf', source_image_time_path) + else: + snapshot_time_path = source_image_time_path + dest_image_path = image_bundle + + try: + self._zvm_images.put_image_to_xcat(dest_image_path, profile) + except exception.ZVMImageError: + with excutils.save_and_reraise_exception(): + new_inst.delete_xcat_node() + + self._zvm_images.clean_up_snapshot_time_path(snapshot_time_path) + + try: + # Pre-config network and create zvm userid + self._preset_instance_network(new_inst._name, network_info) + new_inst.create_userid(block_device_info, image_meta) + + if disk_eph_size_old == 0 and disk_eph_size_new > 0: + # Punch ephemeral disk info to the new instance + zvmutils.punch_eph_info_file(instance_path, new_inst._name) + + # Add nic and deploy the image + self._add_nic_to_instance(new_inst._name, network_info, new_userid) + self._deploy_root_and_ephemeral(new_inst, image_name_xcat) + except exception.ZVMBaseException: + with excutils.save_and_reraise_exception(): + self._zvm_images.delete_image_from_xcat(image_name_xcat) + + if not same_xcat_mn: + try: + for vif in network_info: + self._networkop.clean_mac_switch_host( + new_inst._name) + except exception.ZVMNetworkError: + pass + + new_inst.delete_userid(self._get_hcp_info()['nodename']) + new_inst.delete_xcat_node() + + if same_xcat_mn: + new_inst.copy_xcat_node(old_inst._name) + old_inst.delete_xcat_node() + + # re-configure the networking + self._reconfigure_networking(new_inst._name, network_info, + old_userid) + + if not self._is_nic_granted(new_inst._name): + msg = _("Failed to bound vswitch") + LOG.warn(msg, instance=instance) + else: + if power_on: + new_inst.power_on() + + # Cleanup image from xCAT image repository + self._zvm_images.delete_image_from_xcat(image_name_xcat) + + bdm = driver.block_device_info_get_mapping(block_device_info) + try: + zvmutils.punch_xcat_auth_file(instance_path, new_inst._name) + new_inst.power_on() + self._attach_volume_to_instance(context, instance, bdm) + + if not power_on: + new_inst.power_off() + except exception.ZVMBaseException: + with excutils.save_and_reraise_exception(): + self.destroy(context, instance, network_info, + block_device_info) + if same_xcat_mn: + new_inst.copy_xcat_node(old_inst._name) + old_inst.delete_xcat_node() + + def _reconfigure_networking(self, inst_name, network_info, userid=None): + self._preset_instance_network(inst_name, network_info) + self._add_nic_to_instance(inst_name, network_info, userid) + self._wait_for_nic_update(inst_name) + self._wait_for_addnic(inst_name) + + def _copy_instance(self, instance): + keys = ('name', 'image_ref', 'uuid', 'user_id', 'project_id', + 'power_state', 'system_metadata', 'memory_mb', 'vcpus', + 'root_gb', 'ephemeral_gb') + + inst_copy = {} + for key in keys: + inst_copy[key] = instance[key] + + return inst_copy + + def _attach_volume_to_instance(self, context, instance, + block_device_mapping): + for bd in block_device_mapping: + connection_info = bd['connection_info'] + mountpoint = bd['mount_device'] + self.attach_volume(context, connection_info, instance, mountpoint) + + def _add_nic_to_instance(self, inst_name, network_info, userid=None): + nic_vdev = const.ZVM_DEFAULT_NIC_VDEV + zhcpnode = self._get_hcp_info()['nodename'] + for vif in network_info: + self._networkop.create_nic(zhcpnode, inst_name, + vif['id'], vif['address'], nic_vdev, userid) + nic_vdev = str(hex(int(nic_vdev, 16) + 3))[2:] + + def _deploy_root_and_ephemeral(self, instance, image_name_xcat): + + # Update the nodetype + instance.update_node_info_resize(image_name_xcat) + + # Deploy + instance.deploy_node(image_name_xcat) + + self._zvm_images.update_last_use_date(image_name_xcat) + + def confirm_migration(self, migration, instance, network_info): + """Confirms a resize, destroying the source VM.""" + # Point to old instance + old_instance = self._copy_instance(instance) + old_instance['name'] = ''.join(('rsz', instance['name'])) + old_inst = ZVMInstance(old_instance) + + if self.instance_exists(old_inst._name): + # Same xCAT MN: + self.destroy({}, old_instance) + else: + # Different xCAT MN: + self.destroy({}, instance) + self._zvm_images.cleanup_image_after_migration(instance['name']) + + def finish_revert_migration(self, context, instance, network_info, + block_device_info=None, power_on=True): + """Finish reverting a resize, powering back on the instance.""" + new_instance = self._copy_instance(instance) + new_instance['name'] = ''.join(('rsz', instance['name'])) + zvm_inst = ZVMInstance(new_instance) + bdm = driver.block_device_info_get_mapping(block_device_info) + + if self.instance_exists(zvm_inst._name): + # Same xCAT MN: + old_inst = ZVMInstance(instance) + old_inst.copy_xcat_node(new_instance['name']) + zvm_inst.delete_xcat_node() + + self._reconfigure_networking(instance['name'], network_info) + + if not self._is_nic_granted(instance['name']): + msg = _("Failed to bound vswitch") + LOG.error(msg, instance=instance) + raise nova_exception.MigrationError(reason=msg) + else: + # Different xCAT MN: + self._zvm_images.cleanup_image_after_migration(instance['name']) + + self._attach_volume_to_instance({}, instance, bdm) + + if power_on: + self.power_on({}, instance, []) + + def _wait_for_addnic(self, inst_name): + """Wait until quantum adding NIC done.""" + + def _wait_addnic(inst_name, expiration): + if (CONF.zvm_reachable_timeout and + timeutils.utcnow() > expiration): + raise loopingcall.LoopingCallDone() + + is_done = False + try: + is_done = self._is_nic_granted(inst_name) + except exception.ZVMBaseException: + # Ignore any zvm driver exceptions + return + + if is_done: + LOG.debug(_("NIC added and granted to vswitch correctly")) + raise loopingcall.LoopingCallDone() + + expiration = timeutils.utcnow() + datetime.timedelta( + seconds=CONF.zvm_reachable_timeout) + + timer = loopingcall.FixedIntervalLoopingCall(_wait_addnic, inst_name, + expiration) + timer.start(interval=10).wait() + + def _get_user_directory(self, inst_name): + url = self._xcat_url.lsvm('/' + inst_name) + with zvmutils.expect_invalid_xcat_resp_data(): + dict_str = zvmutils.xcat_request("GET", url)['info'][0][0] + + return dict_str.split("\n") + + def _is_nic_granted(self, inst_name): + dict_list = self._get_user_directory(inst_name) + _all_granted = False + for rec in dict_list: + if " NICDEF " in rec: + _all_granted = " LAN SYSTEM " in rec + if not _all_granted: + return False + + return _all_granted + + def set_admin_password(self, instance, new_pass=None): + """ + Set the root password on the specified instance. + + The first parameter is an instance of nova.compute.service.Instance, + and so the instance is being specified as instance.name. The second + parameter is the value of the new password. + """ + if new_pass is not None: + self._set_admin_password(instance['name'], new_pass) + + def _set_admin_password(self, inst_name, password): + command = "echo %s|passwd --stdin root" % password + try: + zvmutils.xdsh(inst_name, command) + except exception.ZVMXCATXdshFailed as err: + LOG.error(_("Setting root password for instance %(instance)s " + "failed with reason: %(err)s") % + {'instance': inst_name, 'err': err}) + raise err + + def _wait_for_nic_update(self, inst_name): + """Wait until NIC definition is updated""" + + def _wait_revoke(inst_name, expiration): + """Wait until NIC is uncoupled from vswitch""" + if (CONF.zvm_reachable_timeout and + timeutils.utcnow() > expiration): + LOG.warn(_("NIC update check failed.")) + raise loopingcall.LoopingCallDone() + + is_granted = True + try: + is_granted = self._is_nic_granted(inst_name) + except exception.ZVMBaseException: + # Ignore any zvm driver exceptions + return + + if not is_granted: + LOG.debug(_("NIC has been updated")) + raise loopingcall.LoopingCallDone() + + expiration = timeutils.utcnow() + datetime.timedelta( + seconds=CONF.zvm_reachable_timeout) + + timer = loopingcall.FixedIntervalLoopingCall(_wait_revoke, inst_name, + expiration) + timer.start(interval=10).wait() + + return + + def get_console_output(self, context, instance): + """Get console output for an instance""" + + def append_to_log(log_data, log_path): + LOG.debug(_('log_data: %(log_data)r, log_path: %(log_path)r'), + {'log_data': log_data, 'log_path': log_path}) + fp = open(log_path, 'a+') + fp.write(log_data) + return log_path + + zvm_inst = ZVMInstance(instance) + logsize = CONF.zvm_console_log_size * units.Ki + console_log = "" + try: + console_log = zvm_inst.get_console_log(logsize) + except exception.ZVMXCATInternalError: + # Ignore no console log avaiable error + LOG.warn(_("No new console log avaiable.")) + log_path = self._pathutils.get_console_log_path(CONF.zvm_host, + zvm_inst._name) + append_to_log(console_log, log_path) + + log_fp = file(log_path, 'rb') + log_data, remaining = utils.last_bytes(log_fp, logsize) + if remaining > 0: + LOG.info(_('Truncated console log returned, %d bytes ignored'), + remaining, instance=instance) + + return log_data + + def get_host_uptime(self, host_name): + """Get host uptime""" + with zvmutils.expect_invalid_xcat_resp_data(): + return self._host_stats[0]['ipl_time'] diff --git a/nova-zvm-virt-driver/nova/virt/zvm/exception.py b/nova-zvm-virt-driver/nova/virt/zvm/exception.py new file mode 100755 index 0000000..7e636d6 --- /dev/null +++ b/nova-zvm-virt-driver/nova/virt/zvm/exception.py @@ -0,0 +1,80 @@ +# vim: tabstop=4 shiftwidth=4 softtabstop=4 + +# Copyright 2013 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 import exception +from nova.openstack.common.gettextutils import _ + + +class ZVMBaseException(exception.NovaException): + """Base z/VM exception.""" + pass + + +class ZVMDriverError(ZVMBaseException): + msg_fmt = _('z/VM driver error: %(msg)s') + + +class ZVMXCATRequestFailed(ZVMBaseException): + msg_fmt = _('Request to xCAT server %(xcatserver)s failed: %(msg)s') + + +class ZVMInvalidXCATResponseDataError(ZVMBaseException): + msg_fmt = _('Invalid data returned from xCAT: %(msg)s') + + +class ZVMXCATInternalError(ZVMBaseException): + msg_fmt = _('Error returned from xCAT: %(msg)s') + + +class ZVMVolumeError(ZVMBaseException): + msg_fmt = _('Volume error: %(msg)s') + + +class ZVMImageError(ZVMBaseException): + msg_fmt = _("Image error: %(msg)s") + + +class ZVMGetImageFromXCATFailed(ZVMBaseException): + msg_fmt = _('Get image from xCAT failed: %(msg)s') + + +class ZVMNetworkError(ZVMBaseException): + msg_fmt = _("z/VM network error: %(msg)s") + + +class ZVMXCATXdshFailed(ZVMBaseException): + msg_fmt = _('Execute xCAT xdsh command failed: %(msg)s') + + +class ZVMXCATCreateNodeFailed(ZVMBaseException): + msg_fmt = _('Create xCAT node %(node)s failed: %(msg)s') + + +class ZVMXCATCreateUserIdFailed(ZVMBaseException): + msg_fmt = _('Create xCAT user id %(instance)s failed: %(msg)s') + + +class ZVMXCATUpdateNodeFailed(ZVMBaseException): + msg_fmt = _('Update node %(node)s info failed: %(msg)s') + + +class ZVMXCATDeployNodeFailed(ZVMBaseException): + msg_fmt = _('Deploy image on node %(node)s failed: %(msg)s') + + +class ZVMConfigDriveError(ZVMBaseException): + msg_fmt = _('Create configure drive failed: %(msg)s') diff --git a/nova-zvm-virt-driver/nova/virt/zvm/imageop.py b/nova-zvm-virt-driver/nova/virt/zvm/imageop.py new file mode 100755 index 0000000..0022a16 --- /dev/null +++ b/nova-zvm-virt-driver/nova/virt/zvm/imageop.py @@ -0,0 +1,800 @@ +# vim: tabstop=4 shiftwidth=4 softtabstop=4 + +# Copyright 2013 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 commands +import datetime +import os +import re +import shutil +import tarfile +import xml.dom.minidom as Dom + +from oslo.config import cfg + +from nova import exception as nova_exception +from nova.image import glance +from nova.openstack.common import excutils +from nova.openstack.common.gettextutils import _ +from nova.openstack.common import log as logging +from nova.virt import images +from nova.virt.zvm import const +from nova.virt.zvm import exception +from nova.virt.zvm import utils as zvmutils + +LOG = logging.getLogger(__name__) +CONF = cfg.CONF +QUEUE_BUFFER_SIZE = 10 + + +class ZVMImages(object): + + def __init__(self): + self._xcat_url = zvmutils.XCATUrl() + self._pathutils = zvmutils.PathUtils() + + def create_zvm_image(self, instance, image_name, image_href): + """Create z/VM image from z/VM instance by invoking xCAT REST API + imgcapture. + """ + nodename = instance['name'] + profile = image_name + "_" + image_href + body = ['nodename=' + nodename, + 'profile=' + profile] + if CONF.zvm_image_compression_level: + if CONF.zvm_image_compression_level.isdigit() and ( + int(CONF.zvm_image_compression_level) in range(0, 10)): + body.append('compress=%s' % CONF.zvm_image_compression_level) + else: + msg = _("Invalid zvm_image_compression_level detected, please" + "specify it with a integer between 0 and 9 in your nova.conf") + raise exception.ZVMImageError(msg=msg) + + url = self._xcat_url.imgcapture() + LOG.debug(_('Capturing %s start') % instance['name']) + + with zvmutils.except_xcat_call_failed_and_reraise( + exception.ZVMImageError): + res = zvmutils.xcat_request("POST", url, body) + + os_image = self._get_os_image(res) + + return os_image + + def _get_os_image(self, response): + """Return the image_name by parsing the imgcapture rest api + response. + """ + image_name_xcat = "" + if len(response['info']) > 0: + for info in response['info']: + for info_element in info: + if "Completed capturing the image" in info_element: + start_index = info_element.find('(') + end_index = info_element.find(')') + image_name_xcat = info_element[start_index + 1: + end_index] + return image_name_xcat + if len(image_name_xcat) == 0: + msg = _("Capture image failed.") + LOG.error(msg) + raise exception.ZVMImageError(msg=msg) + else: + msg = _("Capture image returns bad response.") + LOG.error(msg) + raise exception.ZVMImageError(msg=msg) + + def get_snapshot_time_path(self): + return self._pathutils.get_snapshot_time_path() + + def get_image_from_xcat(self, image_name_xcat, image_name, + snapshot_time_path): + """Import image from xCAT to nova, by invoking the imgexport + REST API. + """ + LOG.debug(_("Getting image from xCAT")) + destination = os.path.join(snapshot_time_path, image_name + '.tgz') + host = zvmutils.get_host() + body = ['osimage=' + image_name_xcat, + 'destination=' + destination, + 'remotehost=' + host] + url = self._xcat_url.imgexport() + + try: + zvmutils.xcat_request("POST", url, body) + except (exception.ZVMXCATRequestFailed, + exception.ZVMInvalidXCATResponseDataError, + exception.ZVMXCATInternalError) as err: + msg = (_("Transfer image to compute node failed: %s") % err) + raise exception.ZVMImageError(msg=msg) + return destination + + def delete_image_glance(self, image_service, context, image_href): + """ + delete the image from glance database and image repository. + Can be used for a rollback step if operations to image fail. + """ + image_service.delete(context, image_href) + + def clean_up_snapshot_time_path(self, snapshot_time_path): + """ + Clean up the time_path and its contents under "snapshot_tmp" after + image uploaded to glance. + Also be used for a rollback step if operations to the image file fail. + """ + if os.path.exists(snapshot_time_path): + LOG.debug(_("Cleaning up nova local image file")) + shutil.rmtree(snapshot_time_path) + + def _delete_image_file_from_xcat(self, image_name_xcat): + """ + Delete image file from xCAT MN. + When capturing, image in the xCAT MN's repository will be removed after + it is imported to nova compute node. + """ + LOG.debug(_("Removing image files from xCAT MN image repository")) + url = self._xcat_url.rmimage('/' + image_name_xcat) + try: + zvmutils.xcat_request("DELETE", url) + except (exception.ZVMXCATInternalError, + exception.ZVMInvalidXCATResponseDataError, + exception.ZVMXCATRequestFailed): + LOG.warn(_("Failed to delete image file %s from xCAT") % + image_name_xcat) + + def _delete_image_object_from_xcat(self, image_name_xcat): + """Delete image object from xCAT MN. + + After capturing, image definition in the xCAT MN's table osimage and + linuximage will be removed after it is imported to nova compute node. + + """ + LOG.debug(_("Deleting the image object")) + url = self._xcat_url.rmobject('/' + image_name_xcat) + try: + zvmutils.xcat_request("DELETE", url) + except (exception.ZVMXCATInternalError, + exception.ZVMInvalidXCATResponseDataError, + exception.ZVMXCATRequestFailed): + LOG.warn(_("Failed to delete image definition %s from xCAT") % + image_name_xcat) + + def delete_image_from_xcat(self, image_name_xcat): + self._delete_image_file_from_xcat(image_name_xcat) + self._delete_image_object_from_xcat(image_name_xcat) + + def _getxmlnode(self, node, name): + return node.getElementsByTagName(name)[0] if node else [] + + def _getnode(self, node_root, tagname): + """For parse manifest.""" + nodename = node_root.getElementsByTagName(tagname)[0] + nodevalue = nodename.childNodes[0].data + return nodevalue + + def parse_manifest_xml(self, image_package_path): + """Return the image properties from manifest.xml.""" + LOG.debug(_("Parsing the manifest.xml")) + manifest_xml = os.path.join(image_package_path, "manifest.xml") + + manifest = {} + + if os.path.exists(manifest_xml): + xml_file = Dom.parse(manifest_xml) + else: + LOG.warn(_('manifest.xml does not exist')) + manifest['imagename'] = '' + manifest['imagetype'] = '' + manifest['osarch'] = '' + manifest['osname'] = '' + manifest['osvers'] = '' + manifest['profile'] = '' + manifest['provmethod'] = '' + return manifest + + node_root = xml_file.documentElement + node_root = self._getxmlnode(node_root, 'osimage') + manifest['imagename'] = self._getnode(node_root, "imagename") + manifest['imagetype'] = self._getnode(node_root, "imagetype") + manifest['osarch'] = self._getnode(node_root, "osarch") + manifest['osname'] = self._getnode(node_root, "osname") + manifest['osvers'] = self._getnode(node_root, "osvers") + manifest['profile'] = self._getnode(node_root, "profile") + manifest['provmethod'] = self._getnode(node_root, "provmethod") + + return manifest + + def untar_image_bundle(self, snapshot_time_path, image_bundle): + """Untar the image bundle *.tgz from xCAT and remove the *.tgz.""" + if os.path.exists(image_bundle): + LOG.debug(_("Untarring the image bundle ... ")) + tarobj = tarfile.open(image_bundle, "r:gz") + for tarinfo in tarobj: + tarobj.extract(tarinfo.name, path=snapshot_time_path) + tarobj.close() + os.remove(image_bundle) + else: + self.clean_up_snapshot_time_path(snapshot_time_path) + msg = _("Image bundle does not exist") + raise exception.ZVMImageError(msg=msg) + + def get_image_file_name(self, image_package_path): + file_contents = os.listdir(image_package_path) + for f in file_contents: + if f.endswith('.img'): + return f + msg = _("Can not find image file") + raise exception.ZVMImageError(msg=msg) + + def image_exist_xcat(self, image_id): + """To see if the specific image exist in xCAT MN's image + repository. + """ + LOG.debug(_("Checking if the image %s exists or not in xCAT " + "MN's image repository ") % image_id) + image_uuid = image_id.replace('-', '_') + parm = '&criteria=profile=~' + image_uuid + url = self._xcat_url.lsdef_image(addp=parm) + + with zvmutils.except_xcat_call_failed_and_reraise( + exception.ZVMImageError): + res = zvmutils.xcat_request("GET", url) + + res_image = res['info'] + + if '_' in str(res_image): + return True + else: + return False + + def fetch_image(self, context, image_id, target, user, project): + LOG.debug(_("Downloading image %s from glance image server") % + image_id) + try: + images.fetch(context, image_id, target, user, project) + except Exception as err: + msg = _("Download image file of image %(id)s failed with reason:" + " %(err)s") % {'id': image_id, 'err': err} + raise exception.ZVMImageError(msg=msg) + + def generate_manifest_file(self, image_meta, image_name, disk_file, + manifest_path): + """Generate the manifest.xml file from glance's image metadata + as a part of the image bundle. + """ + image_id = image_meta['id'] + image_type = image_meta['properties']['image_type_xcat'] + os_version = image_meta['properties']['os_version'] + os_name = image_meta['properties']['os_name'] + os_arch = image_meta['properties']['architecture'] + prov_method = image_meta['properties']['provisioning_method'] + + image_profile = '_'.join((image_name, image_id.replace('-', '_'))) + image_name_xcat = '-'.join((os_version, os_arch, + prov_method, image_profile)) + rootimgdir_str = ('/install', prov_method, os_version, + os_arch, image_profile) + rootimgdir = '/'.join(rootimgdir_str) + today_date = datetime.date.today() + last_use_date_string = today_date.strftime("%Y-%m-%d") + is_deletable = "auto:last_use_date:" + last_use_date_string + + doc = Dom.Document() + xcatimage = doc.createElement('xcatimage') + doc.appendChild(xcatimage) + + # Add linuximage section + imagename = doc.createElement('imagename') + imagename_value = doc.createTextNode(image_name_xcat) + imagename.appendChild(imagename_value) + rootimagedir = doc.createElement('rootimgdir') + rootimagedir_value = doc.createTextNode(rootimgdir) + rootimagedir.appendChild(rootimagedir_value) + linuximage = doc.createElement('linuximage') + linuximage.appendChild(imagename) + linuximage.appendChild(rootimagedir) + xcatimage.appendChild(linuximage) + + # Add osimage section + osimage = doc.createElement('osimage') + manifest = {'imagename': image_name_xcat, + 'imagetype': image_type, + 'isdeletable': is_deletable, + 'osarch': os_arch, + 'osname': os_name, + 'osvers': os_version, + 'profile': image_profile, + 'provmethod': prov_method} + + for item in manifest.keys(): + itemkey = doc.createElement(item) + itemvalue = doc.createTextNode(manifest[item]) + itemkey.appendChild(itemvalue) + osimage.appendChild(itemkey) + xcatimage.appendChild(osimage) + f = open(manifest_path + '/manifest.xml', 'w') + f.write(doc.toprettyxml(indent='')) + f.close() + + # Add the rawimagefiles section + rawimagefiles = doc.createElement('rawimagefiles') + xcatimage.appendChild(rawimagefiles) + + files = doc.createElement('files') + files_value = doc.createTextNode(rootimgdir + '/' + disk_file) + files.appendChild(files_value) + + rawimagefiles.appendChild(files) + + f = open(manifest_path + '/manifest.xml', 'w') + f.write(doc.toprettyxml(indent=' ')) + f.close() + + self._rewr(manifest_path) + + return manifest_path + '/manifest.xml' + + def _rewr(self, manifest_path): + f = open(manifest_path + '/manifest.xml', 'r') + lines = f.read() + f.close() + + lines = lines.replace('\n', '') + lines = re.sub(r'>(\s*)<', r'>\n\1<', lines) + lines = re.sub(r'>[ \t]*(\S+)[ \t]*<', r'>\1<', lines) + + f = open(manifest_path + '/manifest.xml', 'w') + f.write(lines) + f.close() + + def generate_image_bundle(self, spawn_path, tmp_file_fn, image_name): + """Generate the image bundle which is used to import to xCAT MN's + image repository. + """ + image_bundle_name = image_name + '.tgz' + tar_file = spawn_path + '/' + tmp_file_fn + '_' + image_bundle_name + LOG.debug(_("The generate the image bundle file is %s") % tar_file) + + os.chdir(spawn_path) + tarFile = tarfile.open(tar_file, mode='w:gz') + + try: + tarFile.add(tmp_file_fn) + tarFile.close() + except Exception as err: + msg = (_("Generate image bundle failed: %s") % err) + LOG.error(msg) + if os.path.isfile(tar_file): + os.remove(tar_file) + raise exception.ZVMImageError(msg=msg) + finally: + self._pathutils.clean_temp_folder(tmp_file_fn) + + return tar_file + + def check_space_imgimport_xcat(self, context, instance, tar_file, + xcat_free_space_threshold, zvm_xcat_master): + image_href = instance['image_ref'] + try: + free_space_xcat = self.get_free_space_xcat( + xcat_free_space_threshold, zvm_xcat_master) + img_transfer_needed = self._get_transfer_needed_space_xcat(context, + image_href, tar_file) + larger = max(xcat_free_space_threshold, img_transfer_needed) + if img_transfer_needed > free_space_xcat: + larger = max(xcat_free_space_threshold, img_transfer_needed) + size_needed = float(larger - free_space_xcat) + self.prune_image_xcat(context, size_needed, + img_transfer_needed) + else: + LOG.debug(_("Image transfer needed space satisfied in xCAT")) + except exception.ZVMImageError: + with excutils.save_and_reraise_exception(): + os.remove(tar_file) + + def put_image_to_xcat(self, image_bundle_package, image_profile): + """Import the image bundle from compute node to xCAT MN's image + repository. + """ + remote_host_info = zvmutils.get_host() + body = ['osimage=%s' % image_bundle_package, + 'profile=%s' % image_profile, + 'remotehost=%s' % remote_host_info] + url = self._xcat_url.imgimport() + + try: + zvmutils.xcat_request("POST", url, body) + except (exception.ZVMXCATRequestFailed, + exception.ZVMInvalidXCATResponseDataError, + exception.ZVMXCATInternalError) as err: + msg = _("Import the image bundle to xCAT MN failed: %s") % err + raise exception.ZVMImageError(msg=msg) + finally: + os.remove(image_bundle_package) + + def get_imgname_xcat(self, image_id): + """Get the xCAT deployable image name by image id.""" + image_uuid = image_id.replace('-', '_') + parm = '&criteria=profile=~' + image_uuid + url = self._xcat_url.lsdef_image(addp=parm) + + with zvmutils.except_xcat_call_failed_and_reraise( + exception.ZVMImageError): + res = zvmutils.xcat_request("GET", url) + with zvmutils.expect_invalid_xcat_resp_data(): + res_image = res['info'][0][0] + res_img_name = res_image.strip().split(" ")[0] + + if res_img_name: + return res_img_name + else: + LOG.error(_("Fail to find the right image to deploy")) + + def _get_image_list_xcat(self): + """Get an image list from xcat osimage table. + + criteria: osarch=s390x and provmethod=netboot|raw|sysclone and + isdeletable field + + """ + display_field = '&field=isdeletable' + isdeletable_criteria = '&criteria=isdeletable=~^auto:last_use_date:' + osarch_criteria = '&criteria=osarch=s390x' + provmethod_criteria = '&criteria=provmethod=~netboot|raw|sysclone' + + addp = ''.join([isdeletable_criteria, osarch_criteria, + provmethod_criteria, display_field]) + url = self._xcat_url.lsdef_image(addp=addp) + + with zvmutils.except_xcat_call_failed_and_reraise( + exception.ZVMImageError): + output = zvmutils.xcat_request("GET", url) + + image_list = [] + if len(output['info']) <= 0: + return image_list + + if len(output['info'][0]) > 0: + i = 0 + while i < len(output['info'][0]): + if "Object name:" in output['info'][0][i]: + sub_list = [] + len_objectname = len("Object name: ") + is_deletable = output['info'][0][i + 1] + last_use_date = self._validate_last_use_date( + output['info'][0][i][len_objectname:], + is_deletable) + if last_use_date is None: + i += 2 + continue + sub_list.append(output['info'][0][i][len_objectname:]) + sub_list.append(last_use_date) + image_list.append(sub_list) + i += 2 + + return image_list + + def update_last_use_date(self, image_name_xcat): + """Update the last_use_date in xCAT osimage table after a + successful deploy. + """ + LOG.debug(_("Update the last_use_date in xCAT osimage table " + "after a successful deploy")) + + today_date = datetime.date.today() + last_use_date_string = today_date.strftime("%Y-%m-%d") + url = self._xcat_url.tabch('/osimage') + is_deletable = "auto:last_use_date:" + last_use_date_string + body = ["imagename=" + image_name_xcat, + "osimage.isdeletable=" + is_deletable] + + try: + zvmutils.xcat_request("PUT", url, body) + except (exception.ZVMXCATRequestFailed, + exception.ZVMInvalidXCATResponseDataError, + exception.ZVMXCATInternalError) as err: + LOG.warn(_("Illegal date for last_use_date %s") % err) + + return last_use_date_string + + def _validate_last_use_date(self, image_name, is_deletable): + """Validate the isdeletable date format.""" + last_use_date_string = is_deletable.split(":")[2] + timere = "^\d{4}[-]((0([1-9]{1}))|"\ + "(1[0|1|2]))[-](([0-2]([0-9]{1}))|(3[0|1]))$" + + if (len(last_use_date_string) == 10) and ( + re.match(timere, last_use_date_string)): + LOG.debug(_("The format for last_use_date is valid ")) + else: + LOG.warn(_("The format for image %s record in xcat table osimage's" + " last_used_date is not valid. The correct format is " + "auto:last_use_date:yyyy-mm-dd") % image_name) + return + + try: + last_use_date_datetime = datetime.datetime.strptime( + last_use_date_string, '%Y-%m-%d') + except Exception as err: + LOG.warn(_("Illegal date for last_use_date %(msg)s") % err) + return + + return last_use_date_datetime.date() + + def _verify_is_deletable_periodic(self, last_use_date, clean_period): + """Check the last_use_date of an image to determine if the image + need to be cleaned. + """ + now = datetime.date.today() + delta = (now - last_use_date).days + if (delta - clean_period) >= 0: + return True + else: + return False + + def clean_image_cache_xcat(self, clean_period): + """Clean the old image.""" + image_list = self._get_image_list_xcat() + if len(image_list) <= 0: + return + else: + i = 0 + while i < len(image_list): + image_name_xcat = image_list[i][0] + last_use_date = image_list[i][1] + if self._verify_is_deletable_periodic(last_use_date, + clean_period): + LOG.debug(_('Delete the image %s') % image_name_xcat) + self.delete_image_from_xcat(image_name_xcat) + else: + LOG.debug(_("Keep the image")) + i += 1 + + def _get_image_bundle_size(self, tar_file): + size_byte = os.path.getsize(tar_file) + return float(size_byte) / 1024 / 1024 / 1024 + + def _get_image_size_glance(self, context, image_href): + (image_service, image_id) = glance.get_remote_image_service( + context, image_href) + + try: + image_meta = image_service.show(context, image_href) + except nova_exception.ImageNotFound: + image_meta = {} + return 0 + + size_byte = image_meta['size'] + return float(size_byte) / 1024 / 1024 / 1024 + + def get_free_space_xcat(self, xcat_free_space_threshold, zvm_xcat_master): + """Get the free space in xCAT MN /install.""" + LOG.debug(_("Get the xCAT MN /install free space")) + addp = "&field=--freerepospace" + if isinstance(zvm_xcat_master, str): + url = self._xcat_url.rinv("/" + zvm_xcat_master, addp=addp) + else: + msg = _("zvm_xcat_master should be specified as a string") + LOG.error(msg) + raise exception.ZVMImageError(msg=msg) + + with zvmutils.except_xcat_call_failed_and_reraise( + exception.ZVMImageError): + result = zvmutils.xcat_request("GET", url) + with zvmutils.expect_invalid_xcat_resp_data(): + if len(result['info']) == 0: + msg = _("'rinv --freerepospace' returns " + "null, please check 'df -h /install', there may " + "be something wrong with the mount of /install") + raise exception.ZVMImageError(msg=msg) + free_space_line = result['info'][0][0] + free_space = free_space_line.split()[4] + if free_space.endswith("G"): + free_disk_value = free_space.rstrip("G") + return float(free_disk_value) + elif free_space.endswith("M"): + free_disk_value = free_space.rstrip("M") + return float(free_disk_value) / 1024 + elif free_space == "0": + return 0 + else: + return xcat_free_space_threshold + + def get_imgcapture_needed(self, instance): + """Get the space needed on xCAT MN for an image capture.""" + LOG.debug(_("Getting image capture needed size for %s") % + instance['name']) + + cmd = "df -h /" + result = None + result = zvmutils.xdsh(instance['name'], cmd)['data'][0] + imgcapture_needed_space = "" + try: + result_data = result[0].split() + if CONF.zvm_image_compression_level and \ + int(CONF.zvm_image_compression_level) == 0: + imgcapture_needed_space = result_data[10] + else: + imgcapture_needed_space = result_data[11] + + if imgcapture_needed_space.endswith("G"): + imgcapture_needed_space_value =\ + imgcapture_needed_space.rstrip("G") + return float(imgcapture_needed_space_value) * 2 + elif imgcapture_needed_space.endswith("M"): + imgcapture_needed_space_value =\ + imgcapture_needed_space.rstrip("M") + return (float(imgcapture_needed_space_value) / 1024) * 2 + else: + return const.ZVM_IMAGE_SIZE_MAX + except (IndexError, ValueError, TypeError) as err: + raise exception.ZVMImageError(msg=err) + + def _get_transfer_needed_space_xcat(self, context, image_href, tar_file): + """To transfer an image bundle from glance to xCAT, the needed size is + image_bundle_size + image_size. + """ + image_bundle_size = self._get_image_bundle_size(tar_file) + image_size = self._get_image_size_glance(context, image_href) + return image_bundle_size + image_size + + def _get_image_href_by_osimage(self, image_name_xcat): + """If we have the xCAT.osimage.imagename, we want to get the + image_href. + """ + try: + image_profile = image_name_xcat.split("-") + if len(image_profile) >= 4: + return image_profile[3] + else: + return image_name_xcat + except (TypeError, IndexError): + LOG.error(_("xCAT imagename format for %s is not as expected") + % image_name_xcat) + + def _sort_image_by_use_date(self, image_list, left, right): + """Sort the image_list by last_use_date from oldest image to latest.""" + if (left < right): + i = left + j = right + x = image_list[left] + while (i < j): + while (i < j and image_list[j][1] >= x[1]): + j -= 1 + if(i < j): + image_list[i] = image_list[j] + i += 1 + while(i < j and image_list[i][1] < x[1]): + i += 1 + if(i < j): + image_list[j] = image_list[i] + j -= 1 + image_list[i] = x + self._sort_image_by_use_date(image_list, left, i - 1) + self._sort_image_by_use_date(image_list, i + 1, right) + + def _get_to_be_deleted_images_xcat(self, context, size_needed, + current_needed): + """To get a list of images which is to be removed from xCAT image + repository because it cannot provide enough space for image operations + from OpenStack. + """ + image_list = self._get_image_list_xcat() + size_sum = 0 + to_be_deleted_image_profile = [] + + if len(image_list) <= 0: + msg = _("No image to be deleted, please create space manually " + "on xcat(%s).") % CONF.zvm_xcat_server + raise exception.ZVMImageError(msg=msg) + else: + self._sort_image_by_use_date(image_list, 0, len(image_list) - 1) + for img in image_list: + image_name_xcat = img[0] + image_profile = self._get_image_href_by_osimage( + image_name_xcat) + image_uuid = image_profile.partition('_')[2].replace("_", "-") + image_size = self._get_image_size_glance(context, + image_uuid) + if image_size > 0: + to_be_deleted_image_profile.append(image_profile) + size_sum += image_size + if size_sum >= size_needed: + return to_be_deleted_image_profile + + if size_sum >= current_needed: + return to_be_deleted_image_profile + else: + msg = _("xCAT MN space not enough for the current image operation") + raise exception.ZVMImageError(msg=msg) + + def prune_image_xcat(self, context, size_needed, current_needed): + """Remove the images which meet remove criteria from xCAT.""" + LOG.debug(_("Clear up space by clean images in xCAT")) + to_be_deleted_image_profile = self._get_to_be_deleted_images_xcat( + context, size_needed, current_needed) + if len(to_be_deleted_image_profile) > 0: + for image_profile in to_be_deleted_image_profile: + image_name_xcat = self.get_imgname_xcat(image_profile) + self.delete_image_from_xcat(image_name_xcat) + + def zimage_check(self, image_meta): + """Do a brief check to see if the image is a valid zVM image.""" + property_ = ['image_file_name', 'image_type_xcat', 'architecture', + 'os_name', 'provisioning_method', 'os_version'] + for prop in property_: + if prop not in image_meta['properties'].keys(): + msg = (_("The image %s is not a valid zVM image,please check " + "if the image properties match the requirements.") + % image_meta['id']) + LOG.error(msg) + raise exception.ZVMImageError(msg=msg) + + def cleanup_image_after_migration(self, inst_name): + """Cleanup osimages in xCAT image repository while confirm migration + or revert migration at source compute. + """ + image_list = self._get_image_list_xcat() + matchee = ''.join(['rsz', inst_name]) + for img in image_list: + img_name = img[0] + if matchee in img_name: + self.delete_image_from_xcat(img_name) + + def get_root_disk_units(self, image_file_path): + """use 'hexdump' to get the root_disk_units""" + cmd = "hexdump -n 48 -C %s" % image_file_path + (result, output) = commands.getstatusoutput(cmd) + if result != 0: + msg = (_("Get image property failed," + " please check whether the image file exists!")) + raise exception.ZVMImageError(msg=msg) + + LOG.debug(_("hexdump result is %s") % output) + try: + root_disk_units = int(output[144:156]) + except ValueError: + msg = (_("Image file at %s is missing imbeded disk size " + "metadata, it was probably not captured with xCAT") + % image_file_path) + raise exception.ZVMImageError(msg=msg) + + if 'FBA' not in output and 'CKD' not in output: + msg = (_("The image's disk type is not valid. Currently we only" + " support FBA and CKD disk")) + raise exception.ZVMImageError(msg=msg) + + LOG.debug(_("The image's root_disk_units is %s") % root_disk_units) + return root_disk_units + + def set_image_root_disk_units(self, context, image_meta, image_file_path): + """Set the property 'root_disk_units'to image. """ + new_image_meta = image_meta + root_disk_units = self.get_root_disk_units(image_file_path) + LOG.debug(_("The image's root_disk_units is %s") % root_disk_units) + + (glance_image_service, image_id) = glance.get_remote_image_service( + context, image_meta['id']) + new_image_meta = glance_image_service.show(context, image_id) + new_image_meta['properties']['root_disk_units'] = str(root_disk_units) + + try: + glance_image_service.update(context, image_id, + new_image_meta, None) + except nova_exception.ImageNotAuthorized: + msg = _('Not allowed to modify attributes for image %s') % image_id + LOG.error(msg) + + return new_image_meta diff --git a/nova-zvm-virt-driver/nova/virt/zvm/instance.py b/nova-zvm-virt-driver/nova/virt/zvm/instance.py new file mode 100755 index 0000000..5a6206e --- /dev/null +++ b/nova-zvm-virt-driver/nova/virt/zvm/instance.py @@ -0,0 +1,609 @@ +# vim: tabstop=4 shiftwidth=4 softtabstop=4 + +# Copyright 2013 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 datetime +from oslo.config import cfg + +from nova.compute import power_state +from nova import exception as nova_exception +from nova.openstack.common.gettextutils import _ +from nova.openstack.common import log as logging +from nova.openstack.common import loopingcall +from nova.openstack.common import timeutils +from nova.virt.zvm import const +from nova.virt.zvm import exception +from nova.virt.zvm import utils as zvmutils + +LOG = logging.getLogger(__name__) +CONF = cfg.CONF + + +class ZVMInstance(object): + '''OpenStack instance that running on of z/VM hypervisor.''' + + def __init__(self, instance={}): + """Initialize instance attributes for database.""" + self._xcat_url = zvmutils.XCATUrl() + self._xcat_conn = zvmutils.XCATConnection() + self._instance = instance + self._name = instance['name'] + + def power_off(self): + """Power off z/VM instance.""" + try: + self._power_state("PUT", "off") + except exception.ZVMXCATInternalError as err: + err_str = err.format_message() + if ("Return Code: 200" in err_str and + "Reason Code: 12" in err_str): + # Instance already not active + LOG.warn(_("z/VM instance %s not active") % self._name) + return + else: + msg = _("Failed to power off instance: %s") % err + LOG.error(msg) + raise nova_exception.InstancePowerOffFailure(reason=msg) + + def power_on(self): + """"Power on z/VM instance.""" + try: + self._power_state("PUT", "on") + except exception.ZVMXCATInternalError as err: + err_str = err.format_message() + if ("Return Code: 200" in err_str and + "Reason Code: 8" in err_str): + # Instance already not active + LOG.warn(_("z/VM instance %s already active") % self._name) + return + + self._wait_for_reachable() + if not self._reachable: + LOG.error(_("Failed to power on instance %s: timeout") % + self._name) + raise nova_exception.InstancePowerOnFailure(reason="timeout") + + def reset(self): + """Hard reboot z/VM instance.""" + try: + self._power_state("PUT", "reset") + except exception.ZVMXCATInternalError as err: + err_str = err.format_message() + if ("Return Code: 200" in err_str and + "Reason Code: 12" in err_str): + # Be able to reset in power state of SHUTDOWN + LOG.warn(_("Reset z/VM instance %s from SHUTDOWN state") % + self._name) + return + else: + raise err + self._wait_for_reachable() + + def reboot(self): + """Soft reboot z/VM instance.""" + self._power_state("PUT", "reboot") + self._wait_for_reachable() + + def pause(self): + """Pause the z/VM instance.""" + self._power_state("PUT", "pause") + + def unpause(self): + """Unpause the z/VM instance.""" + self._power_state("PUT", "unpause") + self._wait_for_reachable() + + def attach_volume(self, volumeop, context, connection_info, instance, + mountpoint, is_active, rollback=True): + volumeop.attach_volume_to_instance(context, connection_info, + instance, mountpoint, + is_active, rollback) + + def detach_volume(self, volumeop, connection_info, instance, mountpoint, + is_active, rollback=True): + volumeop.detach_volume_from_instance(connection_info, + instance, mountpoint, + is_active, rollback) + + def get_info(self): + """Get the current status of an z/VM instance. + + Returns a dict containing: + + :state: the running state, one of the power_state codes + :max_mem: (int) the maximum memory in KBytes allowed + :mem: (int) the memory in KBytes used by the domain + :num_cpu: (int) the number of virtual CPUs for the domain + :cpu_time: (int) the CPU time used in nanoseconds + + """ + power_stat = self._get_power_stat() + is_reachable = self.is_reachable() + + max_mem_kb = int(self._instance['memory_mb']) * 1024 + if is_reachable: + try: + rec_list = self._get_rinv_info() + except exception.ZVMXCATInternalError: + raise nova_exception.InstanceNotFound(instance_id=self._name) + + try: + mem = self._get_current_memory(rec_list) + num_cpu = self._get_cpu_count(rec_list) + cpu_time = self._get_cpu_used_time(rec_list) + _instance_info = {'state': power_stat, + 'max_mem': max_mem_kb, + 'mem': mem, + 'num_cpu': num_cpu, + 'cpu_time': cpu_time, } + + except exception.ZVMInvalidXCATResponseDataError: + LOG.warn(_("Failed to get inventory info for %s") % self._name) + _instance_info = {'state': power_stat, + 'max_mem': max_mem_kb, + 'mem': max_mem_kb, + 'num_cpu': self._instance['vcpus'], + 'cpu_time': 0, } + + else: + # Since xCAT rinv can't get info from a server that in power state + # of SHUTDOWN or PAUSED + if ((power_stat == power_state.RUNNING) and + (self._instance['power_state'] == power_state.PAUSED)): + # return paused state only previous power state is paused + _instance_info = {'state': power_state.PAUSED, + 'max_mem': max_mem_kb, + 'mem': max_mem_kb, + 'num_cpu': self._instance['vcpus'], + 'cpu_time': 0, } + else: + # otherwise return xcat returned state + _instance_info = {'state': power_stat, + 'max_mem': max_mem_kb, + 'mem': 0, + 'num_cpu': self._instance['vcpus'], + 'cpu_time': 0, } + return _instance_info + + def create_xcat_node(self, zhcp, userid=None): + """Create xCAT node for z/VM instance.""" + LOG.debug(_("Creating xCAT node for %s") % self._name) + + user_id = userid or self._name + body = ['userid=%s' % user_id, + 'hcp=%s' % zhcp, + 'mgt=zvm', + 'groups=%s' % CONF.zvm_xcat_group] + url = self._xcat_url.mkdef('/' + self._name) + + with zvmutils.except_xcat_call_failed_and_reraise( + exception.ZVMXCATCreateNodeFailed, node=self._name): + zvmutils.xcat_request("POST", url, body) + + def create_userid(self, block_device_info, image_meta): + """Create z/VM userid into user directory for a z/VM instance.""" + # We do not support boot from volume currently + LOG.debug(_("Creating the z/VM user entry for instance %s") + % self._name) + is_volume_base = zvmutils.volume_in_mapping( + const.ZVM_DEFAULT_ROOT_VOLUME, block_device_info) + if is_volume_base: + # TODO(rui): Boot from volume + msg = _("Not support boot from volume.") + raise exception.ZVMXCATCreateUserIdFailed(instance=self._name, + msg=msg) + + eph_disks = block_device_info.get('ephemerals', []) + kwprofile = 'profile=%s' % CONF.zvm_user_profile + body = [kwprofile, + 'password=%s' % CONF.zvm_user_default_password, + 'cpu=%i' % self._instance['vcpus'], + 'memory=%im' % self._instance['memory_mb'], + 'privilege=%s' % CONF.zvm_user_default_privilege] + url = self._xcat_url.mkvm('/' + self._name) + + try: + zvmutils.xcat_request("POST", url, body) + + if not is_volume_base: + size = '%ig' % self._instance['root_gb'] + # use a flavor the disk size is 0 + if size == '0g': + size = image_meta['properties']['root_disk_units'] + # Add root disk and set ipl + self.add_mdisk(CONF.zvm_diskpool, + CONF.zvm_user_root_vdev, + size) + self._set_ipl(CONF.zvm_user_root_vdev) + + # Add additional ephemeral disk + if self._instance['ephemeral_gb'] != 0: + if eph_disks == []: + # Create ephemeral disk according to flavor + fmt = (CONF.default_ephemeral_format or + const.DEFAULT_EPH_DISK_FMT) + self.add_mdisk(CONF.zvm_diskpool, + CONF.zvm_user_adde_vdev, + '%ig' % self._instance['ephemeral_gb'], + fmt) + else: + # Create ephemeral disks according --ephemeral option + for idx, eph in enumerate(eph_disks): + vdev = (eph.get('vdev') or + zvmutils.generate_eph_vdev(idx)) + size = eph['size'] + size_in_units = eph.get('size_in_units', False) + if not size_in_units: + size = '%ig' % size + fmt = (eph.get('guest_format') or + CONF.default_ephemeral_format or + const.DEFAULT_EPH_DISK_FMT) + self.add_mdisk(CONF.zvm_diskpool, vdev, size, fmt) + except (exception.ZVMXCATRequestFailed, + exception.ZVMInvalidXCATResponseDataError, + exception.ZVMXCATInternalError, + exception.ZVMDriverError) as err: + msg = _("Failed to create z/VM userid: %s") % err + LOG.error(msg) + raise exception.ZVMXCATCreateUserIdFailed(instance=self._name, + msg=msg) + + def _set_ipl(self, ipl_state): + body = ["--setipl %s" % ipl_state] + url = self._xcat_url.chvm('/' + self._name) + zvmutils.xcat_request("PUT", url, body) + + def is_locked(self, zhcp_node): + cmd = "smcli Image_Lock_Query_DM -T %s" % self._name + resp = zvmutils.xdsh(zhcp_node, cmd) + + return "is Unlocked..." not in str(resp) + + def _wait_for_unlock(self, zhcp_node, interval=10, timeout=600): + LOG.debug("Waiting for unlock instance %s" % self._name) + + def _wait_unlock(expiration): + if timeutils.utcnow() > expiration: + LOG.debug("Waiting for unlock instance %s timeout" % + self._name) + raise loopingcall.LoopingCallDone() + + if not self.is_locked(zhcp_node): + LOG.debug("Instance %s is unlocked" % + self._name) + raise loopingcall.LoopingCallDone() + + expiration = timeutils.utcnow() + datetime.timedelta(seconds=timeout) + + timer = loopingcall.FixedIntervalLoopingCall(_wait_unlock, + expiration) + timer.start(interval=interval).wait() + + def delete_userid(self, zhcp_node): + """Delete z/VM userid for the instance.This will remove xCAT node + at same time. + """ + url = self._xcat_url.rmvm('/' + self._name) + + try: + zvmutils.xcat_request("DELETE", url) + except exception.ZVMXCATInternalError as err: + if (err.format_message().__contains__("Return Code: 400") and + err.format_message().__contains__("Reason Code: 4")): + # zVM user definition not found, delete xCAT node directly + self.delete_xcat_node() + elif (err.format_message().__contains__("Return Code: 400") and + (err.format_message().__contains__("Reason Code: 16") or + err.format_message().__contains__("Reason Code: 12"))): + # The vm or vm device was locked. Unlock before deleting + self._wait_for_unlock(zhcp_node) + zvmutils.xcat_request("DELETE", url) + else: + raise err + except exception.ZVMXCATRequestFailed as err: + emsg = err.format_message() + if (emsg.__contains__("Invalid nodes and/or groups") and + emsg.__contains__("Forbidden")): + # Assume neither zVM userid nor xCAT node exist in this case + return + else: + raise err + + def delete_xcat_node(self): + """Remove xCAT node for z/VM instance.""" + url = self._xcat_url.rmdef('/' + self._name) + try: + zvmutils.xcat_request("DELETE", url) + except exception.ZVMXCATInternalError as err: + if err.format_message().__contains__("Could not find an object"): + # The xCAT node not exist + return + else: + raise err + + def add_mdisk(self, diskpool, vdev, size, fmt=None): + """Add a 3390 mdisk for a z/VM user. + + NOTE: No read, write and multi password specified, and + access mode default as 'MR'. + + """ + disk_type = CONF.zvm_diskpool_type + if (disk_type == 'ECKD'): + action = '--add3390' + elif (disk_type == 'FBA'): + action = '--add9336' + else: + errmsg = _("Disk type %s is not supported.") % disk_type + LOG.error(errmsg) + raise exception.ZVMDriverError(msg=errmsg) + + if fmt: + body = [" ".join([action, diskpool, vdev, size, "MR", "''", "''", + "''", fmt])] + else: + body = [" ".join([action, diskpool, vdev, size])] + url = self._xcat_url.chvm('/' + self._name) + zvmutils.xcat_request("PUT", url, body) + + def _power_state(self, method, state): + """Invoke xCAT REST API to set/get power state for a instance.""" + body = [state] + url = self._xcat_url.rpower('/' + self._name) + return zvmutils.xcat_request(method, url, body) + + def _get_power_stat(self): + """Get power status of a z/VM instance.""" + LOG.debug(_('Query power stat of %s') % self._name) + res_dict = self._power_state("GET", "stat") + + @zvmutils.wrap_invalid_xcat_resp_data_error + def _get_power_string(d): + tempstr = d['info'][0][0] + return tempstr[(tempstr.find(':') + 2):].strip() + + power_stat = _get_power_string(res_dict) + return zvmutils.mapping_power_stat(power_stat) + + def _get_rinv_info(self): + """get rinv result and return in a list.""" + url = self._xcat_url.rinv('/' + self._name, '&field=cpumem') + LOG.debug(_('Remote inventory of %s') % self._name) + res_info = zvmutils.xcat_request("GET", url)['info'] + + with zvmutils.expect_invalid_xcat_resp_data(): + rinv_info = res_info[0][0].split('\n') + + return rinv_info + + @zvmutils.wrap_invalid_xcat_resp_data_error + def _modify_storage_format(self, mem): + """modify storage from 'G' ' M' to 'K'.""" + new_mem = 0 + if mem.endswith('G'): + new_mem = int(mem[:-1]) * 1024 * 1024 + elif mem.endswith('M'): + new_mem = int(mem[:-1]) * 1024 + elif mem.endswith('K'): + new_mem = int(mem[:-1]) + else: + exp = "ending with a 'G', 'M' or 'K'" + errmsg = _("Invalid memory format: %(invalid)s; Expected: " + "%(exp)s") % {'invalid': mem, 'exp': exp} + LOG.error(errmsg) + raise exception.ZVMInvalidXCATResponseDataError(msg=errmsg) + return new_mem + + @zvmutils.wrap_invalid_xcat_resp_data_error + def _get_current_memory(self, rec_list): + """Return the max memory can be used.""" + _mem = None + + for rec in rec_list: + if rec.__contains__("Total Memory: "): + tmp_list = rec.split() + _mem = tmp_list[3] + + _mem = self._modify_storage_format(_mem) + return _mem + + @zvmutils.wrap_invalid_xcat_resp_data_error + def _get_cpu_count(self, rec_list): + """Return the virtual cpu count.""" + _cpu_flag = False + num_cpu = 0 + + for rec in rec_list: + if (_cpu_flag is True): + tmp_list = rec.split() + if (len(tmp_list) > 1): + if (tmp_list[1] == "CPU"): + num_cpu += 1 + else: + _cpu_flag = False + if rec.__contains__("Processors: "): + _cpu_flag = True + + return num_cpu + + @zvmutils.wrap_invalid_xcat_resp_data_error + def _get_cpu_used_time(self, rec_list): + """Return the cpu used time in.""" + cpu_time = None + + for rec in rec_list: + if rec.__contains__("CPU Used Time: "): + tmp_list = rec.split() + cpu_time = tmp_list[4] + + return int(cpu_time) + + def is_reachable(self): + """Return True is the instance is reachable.""" + url = self._xcat_url.nodestat('/' + self._name) + LOG.debug(_('Get instance status of %s') % self._name) + res_dict = zvmutils.xcat_request("GET", url) + + with zvmutils.expect_invalid_xcat_resp_data(): + status = res_dict['node'][0][0]['data'][0] + + if status is not None: + if status.__contains__('sshd'): + return True + + return False + + def _wait_for_reachable(self): + """Called at an interval until the instance is reachable.""" + self._reachable = False + + def _wait_reachable(expiration): + if (CONF.zvm_reachable_timeout and + timeutils.utcnow() > expiration): + raise loopingcall.LoopingCallDone() + + if self.is_reachable(): + self._reachable = True + LOG.debug(_("Instance %s reachable now") % + self._name) + raise loopingcall.LoopingCallDone() + + expiration = timeutils.utcnow() + datetime.timedelta( + seconds=CONF.zvm_reachable_timeout) + + timer = loopingcall.FixedIntervalLoopingCall(_wait_reachable, + expiration) + timer.start(interval=5).wait() + + def update_node_info(self, image_meta): + LOG.debug(_("Update the node info for instance %s") % self._name) + + image_name = image_meta['name'] + image_id = image_meta['id'] + os_type = image_meta['properties']['os_version'] + os_arch = image_meta['properties']['architecture'] + prov_method = image_meta['properties']['provisioning_method'] + profile_name = '_'.join((image_name, image_id.replace('-', '_'))) + + body = ['noderes.netboot=%s' % const.HYPERVISOR_TYPE, + 'nodetype.os=%s' % os_type, + 'nodetype.arch=%s' % os_arch, + 'nodetype.provmethod=%s' % prov_method, + 'nodetype.profile=%s' % profile_name] + url = self._xcat_url.chtab('/' + self._name) + + with zvmutils.except_xcat_call_failed_and_reraise( + exception.ZVMXCATUpdateNodeFailed, node=self._name): + zvmutils.xcat_request("PUT", url, body) + + def update_node_info_resize(self, image_name_xcat): + LOG.debug(_("Update the nodetype for instance %s") % self._name) + + name_section = image_name_xcat.split("-") + os_type = name_section[0] + os_arch = name_section[1] + profile_name = name_section[3] + + body = ['noderes.netboot=%s' % const.HYPERVISOR_TYPE, + 'nodetype.os=%s' % os_type, + 'nodetype.arch=%s' % os_arch, + 'nodetype.provmethod=%s' % 'sysclone', + 'nodetype.profile=%s' % profile_name] + + url = self._xcat_url.chtab('/' + self._name) + + with zvmutils.except_xcat_call_failed_and_reraise( + exception.ZVMXCATUpdateNodeFailed, node=self._name): + zvmutils.xcat_request("PUT", url, body) + + def get_provmethod(self): + addp = "&col=node=%s&attribute=provmethod" % self._name + url = self._xcat_url.gettab('/nodetype', addp) + res_info = zvmutils.xcat_request("GET", url) + return res_info['data'][0][0] + + def update_node_provmethod(self, provmethod): + LOG.debug(_("Update the nodetype for instance %s") % self._name) + + body = ['nodetype.provmethod=%s' % provmethod] + + url = self._xcat_url.chtab('/' + self._name) + + with zvmutils.except_xcat_call_failed_and_reraise( + exception.ZVMXCATUpdateNodeFailed, node=self._name): + zvmutils.xcat_request("PUT", url, body) + + def update_node_def(self, hcp, userid): + """Update xCAT node definition.""" + + body = ['zvm.hcp=%s' % hcp, + 'zvm.userid=%s' % userid] + url = self._xcat_url.chtab('/' + self._name) + + with zvmutils.except_xcat_call_failed_and_reraise( + exception.ZVMXCATUpdateNodeFailed, node=self._name): + zvmutils.xcat_request("PUT", url, body) + + def deploy_node(self, image_name, transportfiles=None, vdev=None): + LOG.debug(_("Begin to deploy image on instance %s") % self._name) + vdev = vdev or CONF.zvm_user_root_vdev + remote_host_info = zvmutils.get_host() + body = ['netboot', + 'device=%s' % vdev, + 'osimage=%s' % image_name] + + if transportfiles: + body.append('transport=%s' % transportfiles) + body.append('remotehost=%s' % remote_host_info) + + url = self._xcat_url.nodeset('/' + self._name) + + with zvmutils.except_xcat_call_failed_and_reraise( + exception.ZVMXCATDeployNodeFailed, node=self._name): + zvmutils.xcat_request("PUT", url, body) + + def copy_xcat_node(self, source_node_name): + """Create xCAT node from an existing z/VM instance.""" + LOG.debug(_("Creating xCAT node %s from existing node") % self._name) + + url = self._xcat_url.lsdef_node('/' + source_node_name) + res_info = zvmutils.xcat_request("GET", url)['info'][0] + + body = [] + for info in res_info: + if "=" in info and ("postbootscripts" not in info)\ + and ("postscripts" not in info) \ + and ("hostnames" not in info): + body.append(info.lstrip()) + + url = self._xcat_url.mkdef('/' + self._name) + + with zvmutils.except_xcat_call_failed_and_reraise( + exception.ZVMXCATCreateNodeFailed, node=self._name): + zvmutils.xcat_request("POST", url, body) + + def get_console_log(self, logsize): + """get console log.""" + url = self._xcat_url.rinv('/' + self._name, '&field=console' + '&field=%s') % logsize + + LOG.debug(_('Get console log of %s') % self._name) + res_info = zvmutils.xcat_request("GET", url)['info'] + + with zvmutils.expect_invalid_xcat_resp_data(): + rinv_info = res_info[0][0] + + return rinv_info diff --git a/nova-zvm-virt-driver/nova/virt/zvm/networkop.py b/nova-zvm-virt-driver/nova/virt/zvm/networkop.py new file mode 100755 index 0000000..003c4b4 --- /dev/null +++ b/nova-zvm-virt-driver/nova/virt/zvm/networkop.py @@ -0,0 +1,207 @@ +# vim: tabstop=4 shiftwidth=4 softtabstop=4 + +# Copyright 2013 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 + +from nova.openstack.common.gettextutils import _ +from nova.openstack.common import importutils +from nova.openstack.common import log as logging +from nova.virt.zvm import exception +from nova.virt.zvm import utils as zvmutils + + +LOG = logging.getLogger(__name__) + +CONF = cfg.CONF + +NetworkUtils = zvmutils.NetworkUtils() + + +class NetworkOperator(object): + """Configuration check and manage MAC address.""" + + _vif_driver_class_map = { + 'nova.network.neutronv2.api.API': + 'nova.virt.zvm.vif.ZVMNeutronVIFDriver', + } + + def __init__(self): + self._xcat_url = zvmutils.XCATUrl() + self._load_vif_driver_class() + + def _load_vif_driver_class(self): + vif_driver = CONF.network_api_class + try: + class_name = self._vif_driver_class_map[vif_driver] + except KeyError: + msg = _("VIF driver %s not supported by z/VM.") % vif_driver + LOG.error(msg) + raise exception.ZVMNetworkError(msg=msg) + + self._vif_driver = importutils.import_object(class_name) + + def add_xcat_host(self, node, ip, host_name): + """Add/Update hostname/ip bundle in xCAT MN nodes table.""" + commands = "node=%s" % node + " hosts.ip=%s" % ip + commands += " hosts.hostnames=%s" % host_name + body = [commands] + url = self._xcat_url.tabch("/hosts") + + with zvmutils.except_xcat_call_failed_and_reraise( + exception.ZVMNetworkError): + result_data = zvmutils.xcat_request("PUT", url, body)['data'] + + return result_data + + def _delete_xcat_host(self, node_name): + """Remove xcat hosts table rows where node name is node_name.""" + commands = "-d node=%s hosts" % node_name + body = [commands] + url = self._xcat_url.tabch("/hosts") + + with zvmutils.except_xcat_call_failed_and_reraise( + exception.ZVMNetworkError): + return zvmutils.xcat_request("PUT", url, body)['data'] + + def add_xcat_mac(self, node, interface, mac, zhcp=None): + """Add node name, interface, mac address into xcat mac table.""" + commands = "mac.node=%s" % node + " mac.mac=%s" % mac + commands += " mac.interface=%s" % interface + if zhcp is not None: + commands += " mac.comments=%s" % zhcp + url = self._xcat_url.tabch("/mac") + body = [commands] + + with zvmutils.except_xcat_call_failed_and_reraise( + exception.ZVMNetworkError): + return zvmutils.xcat_request("PUT", url, body)['data'] + + def add_xcat_switch(self, node, nic_name, interface, zhcp=None): + """Add node name and nic name address into xcat switch table.""" + commands = "switch.node=%s" % node + commands += " switch.port=%s" % nic_name + commands += " switch.interface=%s" % interface + if zhcp is not None: + commands += " switch.comments=%s" % zhcp + url = self._xcat_url.tabch("/switch") + body = [commands] + + with zvmutils.except_xcat_call_failed_and_reraise( + exception.ZVMNetworkError): + return zvmutils.xcat_request("PUT", url, body)['data'] + + def _delete_xcat_mac(self, node_name): + """Remove node mac record from xcat mac table.""" + commands = "-d node=%s mac" % node_name + url = self._xcat_url.tabch("/mac") + body = [commands] + + with zvmutils.except_xcat_call_failed_and_reraise( + exception.ZVMNetworkError): + return zvmutils.xcat_request("PUT", url, body)['data'] + + def _delete_xcat_switch(self, node_name): + """Remove node switch record from xcat switch table.""" + commands = "-d node=%s switch" % node_name + url = self._xcat_url.tabch("/switch") + body = [commands] + + with zvmutils.except_xcat_call_failed_and_reraise( + exception.ZVMNetworkError): + return zvmutils.xcat_request("PUT", url, body)['data'] + + def update_xcat_mac(self, node, interface, mac, zhcp=None): + """Add node name, interface, mac address into xcat mac table.""" + commands = "node=%s" % node + " interface=%s" % interface + commands += " mac.mac=%s" % mac + if zhcp is not None: + commands += " mac.comments=%s" % zhcp + url = self._xcat_url.tabch("/mac") + body = [commands] + + with zvmutils.except_xcat_call_failed_and_reraise( + exception.ZVMNetworkError): + return zvmutils.xcat_request("PUT", url, body)['data'] + + def update_xcat_switch(self, node, nic_name, interface, zhcp=None): + """Add node name and nic name address into xcat switch table.""" + commands = "node=%s" % node + commands += " interface=%s" % interface + commands += " switch.port=%s" % nic_name + if zhcp is not None: + commands += " switch.comments=%s" % zhcp + url = self._xcat_url.tabch("/switch") + body = [commands] + + with zvmutils.except_xcat_call_failed_and_reraise( + exception.ZVMNetworkError): + return zvmutils.xcat_request("PUT", url, body)['data'] + + def clean_mac_switch_host(self, node_name): + """Clean node records in xCAT mac, host and switch table.""" + self.clean_mac_switch(node_name) + self._delete_xcat_host(node_name) + + def clean_mac_switch(self, node_name): + """Clean node records in xCAT mac and switch table.""" + self._delete_xcat_mac(node_name) + self._delete_xcat_switch(node_name) + + def makehosts(self): + """Update xCAT MN /etc/hosts file.""" + url = self._xcat_url.network("/makehosts") + with zvmutils.except_xcat_call_failed_and_reraise( + exception.ZVMNetworkError): + return zvmutils.xcat_request("PUT", url)['data'] + + def makeDNS(self): + """Update xCAT MN DNS.""" + url = self._xcat_url.network("/makedns") + with zvmutils.except_xcat_call_failed_and_reraise( + exception.ZVMNetworkError): + return zvmutils.xcat_request("PUT", url)['data'] + + def config_xcat_mac(self, instance_name): + """Hook xCat to prevent assign MAC for instance.""" + fake_mac_addr = "00:00:00:00:00:00" + nic_name = "fake" + self.add_xcat_mac(instance_name, nic_name, fake_mac_addr) + + def create_nic(self, zhcpnode, inst_name, nic_name, mac_address, vdev, + userid=None): + """Create network information in xCAT and zVM user direct.""" + macid = mac_address.replace(':', '')[-6:] + self._add_instance_nic(zhcpnode, inst_name, vdev, macid, userid) + self._delete_xcat_mac(inst_name) + self.add_xcat_mac(inst_name, vdev, mac_address, zhcpnode) + self.add_xcat_switch(inst_name, nic_name, vdev, zhcpnode) + + def _add_instance_nic(self, zhcpnode, inst_name, vdev, macid, userid=None): + """Add NIC defination into user direct.""" + if userid is None: + command = ("/opt/zhcp/bin/smcli Image_Definition_Update_DM -T %s" % + inst_name) + else: + command = ("/opt/zhcp/bin/smcli Image_Definition_Update_DM -T %s" % + userid) + command += " -k \'NICDEF=VDEV=%s TYPE=QDIO " % vdev + command += "MACID=%s\'" % macid + try: + zvmutils.xdsh(zhcpnode, command) + except exception.ZVMXCATXdshFailed as err: + msg = _("Adding nic error: %s") % err + raise exception.ZVMNetworkError(msg=msg) diff --git a/nova-zvm-virt-driver/nova/virt/zvm/utils.py b/nova-zvm-virt-driver/nova/virt/zvm/utils.py new file mode 100755 index 0000000..a63c1e7 --- /dev/null +++ b/nova-zvm-virt-driver/nova/virt/zvm/utils.py @@ -0,0 +1,830 @@ +# vim: tabstop=4 shiftwidth=4 softtabstop=4 + +# Copyright 2013 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 os +import shutil +import socket +import time + +from oslo.config import cfg + +from nova import block_device +from nova.compute import power_state +from nova.openstack.common import excutils +from nova.openstack.common.gettextutils import _ +from nova.openstack.common import jsonutils +from nova.openstack.common import log as logging +from nova.virt import driver +from nova.virt.zvm import const +from nova.virt.zvm import exception + + +LOG = logging.getLogger(__name__) + +CONF = cfg.CONF +CONF.import_opt('instances_path', 'nova.compute.manager') + + +class XCATUrl(object): + """To return xCAT url for invoking xCAT REST API.""" + + def __init__(self): + """Set constant that used to form xCAT url.""" + self.PREFIX = '/xcatws' + self.SUFFIX = '?userName=' + CONF.zvm_xcat_username + \ + '&password=' + CONF.zvm_xcat_password + \ + '&format=json' + + self.NODES = '/nodes' + self.VMS = '/vms' + self.IMAGES = '/images' + self.OBJECTS = '/objects/osimage' + self.OS = '/OS' + self.TABLES = '/tables' + self.HV = '/hypervisor' + self.NETWORK = '/networks' + + self.POWER = '/power' + self.INVENTORY = '/inventory' + self.STATUS = '/status' + self.MIGRATE = '/migrate' + self.CAPTURE = '/capture' + self.EXPORT = '/export' + self.IMGIMPORT = '/import' + self.BOOTSTAT = '/bootstate' + self.XDSH = '/dsh' + + def _nodes(self, arg=''): + return self.PREFIX + self.NODES + arg + self.SUFFIX + + def _vms(self, arg=''): + return self.PREFIX + self.VMS + arg + self.SUFFIX + + def _hv(self, arg=''): + return self.PREFIX + self.HV + arg + self.SUFFIX + + def rpower(self, arg=''): + return self.PREFIX + self.NODES + arg + self.POWER + self.SUFFIX + + def nodels(self, arg=''): + return self._nodes(arg) + + def rinv(self, arg='', addp=None): + rurl = self.PREFIX + self.NODES + arg + self.INVENTORY + self.SUFFIX + return self._append_addp(rurl, addp) + + def mkdef(self, arg=''): + return self._nodes(arg) + + def rmdef(self, arg=''): + return self._nodes(arg) + + def nodestat(self, arg=''): + return self.PREFIX + self.NODES + arg + self.STATUS + self.SUFFIX + + def chvm(self, arg=''): + return self._vms(arg) + + def lsvm(self, arg=''): + return self._vms(arg) + + def chhv(self, arg=''): + return self._hv(arg) + + def mkvm(self, arg=''): + return self._vms(arg) + + def rmvm(self, arg=''): + return self._vms(arg) + + def tabdump(self, arg='', addp=None): + rurl = self.PREFIX + self.TABLES + arg + self.SUFFIX + return self._append_addp(rurl, addp) + + def _append_addp(self, rurl, addp=None): + if addp is not None: + return rurl + addp + else: + return rurl + + def imgcapture(self, arg=''): + return self.PREFIX + self.IMAGES + arg + self.CAPTURE + self.SUFFIX + + def imgexport(self, arg=''): + return self.PREFIX + self.IMAGES + arg + self.EXPORT + self.SUFFIX + + def rmimage(self, arg=''): + return self.PREFIX + self.IMAGES + arg + self.SUFFIX + + def rmobject(self, arg=''): + return self.PREFIX + self.OBJECTS + arg + self.SUFFIX + + def lsdef_node(self, arg='', addp=None): + rurl = self.PREFIX + self.NODES + arg + self.SUFFIX + return self._append_addp(rurl, addp) + + def lsdef_image(self, arg='', addp=None): + rurl = self.PREFIX + self.IMAGES + arg + self.SUFFIX + return self._append_addp(rurl, addp) + + def imgimport(self, arg=''): + return self.PREFIX + self.IMAGES + self.IMGIMPORT + arg + self.SUFFIX + + def chtab(self, arg=''): + return self.PREFIX + self.NODES + arg + self.SUFFIX + + def nodeset(self, arg=''): + return self.PREFIX + self.NODES + arg + self.BOOTSTAT + self.SUFFIX + + def rmigrate(self, arg=''): + return self.PREFIX + self.NODES + arg + self.MIGRATE + self.SUFFIX + + def gettab(self, arg='', addp=None): + rurl = self.PREFIX + self.TABLES + arg + self.SUFFIX + return self._append_addp(rurl, addp) + + def tabch(self, arg='', addp=None): + """Add/update/delete row(s) in table arg, with attribute addp.""" + rurl = self.PREFIX + self.TABLES + arg + self.SUFFIX + return self._append_addp(rurl, addp) + + def xdsh(self, arg=''): + """Run shell command.""" + return self.PREFIX + self.NODES + arg + self.XDSH + self.SUFFIX + + def network(self, arg='', addp=None): + rurl = self.PREFIX + self.NETWORK + arg + self.SUFFIX + if addp is not None: + return rurl + addp + else: + return rurl + + +class XCATConnection(): + """Https requests to xCAT web service.""" + + def __init__(self): + """Initialize https connection to xCAT service.""" + self.host = CONF.zvm_xcat_server + self.conn = httplib.HTTPSConnection(self.host, + timeout=CONF.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)} + + try: + self.conn.request(method, url, body, headers) + except socket.gaierror as err: + msg = _("Failed to find address: %s") % err + raise exception.ZVMXCATRequestFailed(xcatserver=self.host, msg=msg) + except (socket.error, socket.timeout) as err: + msg = _("Communication error: %s") % err + raise exception.ZVMXCATRequestFailed(xcatserver=self.host, msg=msg) + + try: + res = self.conn.getresponse() + except Exception as err: + msg = _("Failed to get response from xCAT: %s") % err + raise exception.ZVMXCATRequestFailed(xcatserver=self.host, msg=msg) + + msg = res.read() + resp = { + 'status': res.status, + 'reason': res.reason, + 'message': msg} + + # 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: + raise exception.ZVMXCATRequestFailed(xcatserver=self.host, + msg=err) + + 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 exception.ZVMDriverError(msg=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: + raise exception.ZVMInvalidXCATResponseDataError(msg=err) + + +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: + raise exception.ZVMInvalidXCATResponseDataError(msg=err) + + return decorated_function + + +@contextlib.contextmanager +def ignore_errors(): + """Only execute the clauses and ignore the results.""" + + try: + yield + except Exception as err: + LOG.debug(_("Ignore an error: %s") % err) + pass + + +@contextlib.contextmanager +def except_xcat_call_failed_and_reraise(exc, **kwargs): + """Catch all kinds of xCAT call failure and reraise. + + exc: the exception that would be raised. + """ + try: + yield + except (exception.ZVMXCATRequestFailed, + exception.ZVMInvalidXCATResponseDataError, + exception.ZVMXCATInternalError) as err: + kwargs['msg'] = err + raise exc(**kwargs) + + +def convert_to_mb(s): + """Convert memory size from GB to MB.""" + s = s.upper() + try: + if s.endswith('G'): + return float(s[:-1].strip()) * 1024 + else: + return float(s[:-1].strip()) + except (IndexError, ValueError, KeyError, TypeError) as e: + errmsg = _("Invalid memory format: %s") % e + raise exception.ZVMDriverError(msg=errmsg) + + +@wrap_invalid_xcat_resp_data_error +def translate_xcat_resp(rawdata, dirt): + """Translate xCAT response JSON stream to a python dictionary. + + xCAT response example: + node: keyword1: value1\n + node: keyword2: value2\n + ... + node: keywordn: valuen\n + + Will return a python dictionary: + {keyword1: value1, + keyword2: value2, + ... + keywordn: valuen,} + + """ + 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 exception.ZVMInvalidXCATResponseDataError(msg=msg) + + return data + + +def mapping_power_stat(power_stat): + """Translate power state to OpenStack defined constants.""" + return const.ZVM_POWER_STAT.get(power_stat, power_state.NOSTATE) + + +@wrap_invalid_xcat_resp_data_error +def load_xcat_resp(message): + """Abstract information from xCAT REST response body. + + As default, xCAT response will in format of JSON and can be + converted to Python dictionary, would looks like: + {"data": [{"info": [info,]}, {"data": [data,]}, ..., {"error": [error,]}]} + + Returns a Python dictionary, looks like: + {'info': [info,], + 'data': [data,], + ... + 'error': [error,]} + + """ + resp_list = jsonloads(message)['data'] + keys = const.XCAT_RESPONSE_KEYS + + 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 + continue + else: + raise exception.ZVMXCATInternalError(msg=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 volume_in_mapping(mount_device, block_device_info): + block_device_list = [block_device.strip_dev(vol['mount_device']) + for vol in + driver.block_device_info_get_mapping( + block_device_info)] + swap = driver.block_device_info_get_swap(block_device_info) + if driver.swap_is_usable(swap): + block_device_list.append( + block_device.strip_dev(swap['device_name'])) + block_device_list += [block_device.strip_dev(ephemeral['device_name']) + for ephemeral in + driver.block_device_info_get_ephemerals( + block_device_info)] + + LOG.debug(_("block_device_list %s"), block_device_list) + return block_device.strip_dev(mount_device) in block_device_list + + +def get_host(): + return ''.join([os.environ["USER"], '@', CONF.my_ip]) + + +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) + + with except_xcat_call_failed_and_reraise( + exception.ZVMXCATXdshFailed): + res_dict = xdsh_execute(node, commands) + + return res_dict + + +def punch_file(node, fn, fclass): + body = [" ".join(['--punchfile', fn, fclass, get_host()])] + url = XCATUrl().chvm('/' + node) + + try: + xcat_request("PUT", url, body) + except Exception as err: + with excutils.save_and_reraise_exception(): + LOG.error(_('Punch file to %(node)s failed: %(msg)s') % + {'node': node, 'msg': err}) + finally: + os.remove(fn) + + +def punch_adminpass_file(instance_path, instance_name, admin_password): + adminpass_fn = ''.join([instance_path, '/adminpwd.sh']) + _generate_adminpass_file(adminpass_fn, admin_password) + punch_file(instance_name, adminpass_fn, 'X') + + +def punch_xcat_auth_file(instance_path, instance_name): + """Make xCAT MN authorized by virtual machines.""" + mn_pub_key = get_mn_pub_key() + auth_fn = ''.join([instance_path, '/xcatauth.sh']) + _generate_auth_file(auth_fn, mn_pub_key) + punch_file(instance_name, auth_fn, 'X') + + +def punch_eph_info_file(instance_path, instance_name, vdev=None, fmt=None, + mntdir=None): + if not fmt: + fmt = CONF.default_ephemeral_format or const.DEFAULT_EPH_DISK_FMT + eph_fn = ''.join([instance_path, '/eph.disk']) + _generate_ephinfo_file(eph_fn, vdev, fmt, mntdir) + punch_file(instance_name, eph_fn, 'X') + + +def generate_vdev(base, offset=1): + """Generate virtual device number base on base vdev. + + :param base: base virtual device number, string of 4 bit hex. + :param offset: offset to base, integer. + + :output: virtual device number, string of 4 bit hex. + """ + vdev = hex(int(base, 16) + offset)[2:] + return vdev.rjust(4, '0') + + +def generate_eph_vdev(offset=1): + """Generate virtual device number for ephemeral disks. + + :parm offset: offset to zvm_user_adde_vdev. + + :output: virtual device number, string of 4 bit hex. + """ + vdev = generate_vdev(CONF.zvm_user_adde_vdev, offset + 1) + if offset >= 0 and offset < 254: + return vdev + else: + msg = _("Invalid virtual device number for ephemeral disk: %s") % vdev + LOG.error(msg) + raise exception.ZVMDriverError(msg=msg) + + +def _generate_ephinfo_file(fname, vdev, fmt, mntdir): + vdev = vdev or CONF.zvm_user_adde_vdev + mntdir = mntdir or CONF.zvm_default_ephemeral_mntdir + + lines = [ + '# xCAT Init\n', + 'action=addMdisk\n', + 'vaddr=' + vdev + '\n', + 'filesys=' + fmt + '\n', + 'mntdir=' + mntdir + '\n' + ] + + with open(fname, 'w') as genfile: + try: + genfile.writelines(lines) + except Exception as err: + with excutils.save_and_reraise_exception(): + LOG.error(_('Generate ephemeral info file failed: %s') % err) + + +def _generate_auth_file(fn, pub_key): + lines = ['#!/bin/bash\n', + 'echo "%s" >> /root/.ssh/authorized_keys' % pub_key] + with open(fn, 'w') as f: + f.writelines(lines) + + +def _generate_adminpass_file(fn, admin_password): + lines = ['#! /bin/bash\n', + 'echo %s|passwd --stdin root' % admin_password] + with open(fn, 'w') as f: + f.writelines(lines) + + +@wrap_invalid_xcat_resp_data_error +def get_mn_pub_key(): + cmd = 'cat /root/.ssh/id_rsa.pub' + resp = xdsh(CONF.zvm_xcat_master, cmd) + key = resp['data'][0][0] + start_idx = key.find('ssh-rsa') + key = key[start_idx:] + return key + + +def parse_os_version(os_version): + """Separate os and version from os_version. + Possible return value are only: + ('rhel', x.y) and ('sles', x.y) where x.y may not be digits + """ + supported = {'rhel': ['rhel', 'redhat', 'red hat'], + 'sles': ['suse', 'sles']} + os_version = os_version.lower() + for distro, patterns in supported.items(): + for i in patterns: + if os_version.startswith(i): + # Not guarrentee the version is digital + return distro, os_version.split(i, 2)[1] + else: + raise exception.ZVMImageError(msg='Unknown os_version property') + + +class PathUtils(object): + def open(self, path, mode): + """Wrapper on __builin__.open used to simplify unit testing.""" + import __builtin__ + return __builtin__.open(path, mode) + + def _get_image_tmp_path(self): + image_tmp_path = os.path.normpath(CONF.zvm_image_tmp_path) + if not os.path.exists(image_tmp_path): + LOG.debug(_('Creating folder %s for image temp files') % + image_tmp_path) + os.makedirs(image_tmp_path) + return image_tmp_path + + def get_bundle_tmp_path(self, tmp_file_fn): + bundle_tmp_path = os.path.join(self._get_image_tmp_path(), "spawn_tmp", + tmp_file_fn) + if not os.path.exists(bundle_tmp_path): + LOG.debug(_('Creating folder %s for image bundle temp file') % + bundle_tmp_path) + os.makedirs(bundle_tmp_path) + return bundle_tmp_path + + def get_img_path(self, bundle_file_path, image_name): + return os.path.join(bundle_file_path, image_name) + + def _get_snapshot_path(self): + snapshot_folder = os.path.join(self._get_image_tmp_path(), + "snapshot_tmp") + if not os.path.exists(snapshot_folder): + LOG.debug(_("Creating the snapshot folder %s") % snapshot_folder) + os.makedirs(snapshot_folder) + return snapshot_folder + + def _get_punch_path(self): + punch_folder = os.path.join(self._get_image_tmp_path(), "punch_tmp") + if not os.path.exists(punch_folder): + LOG.debug(_("Creating the punch folder %s") % punch_folder) + os.makedirs(punch_folder) + return punch_folder + + def _make_short_time_stamp(self): + # tmp_file_fn = time.strftime('%d%H%M%S', + # time.localtime(time.time())) + # return tmp_file_fn + return '0' + + def get_punch_time_path(self): + punch_time_path = os.path.join(self._get_punch_path(), + self._make_short_time_stamp()) + if not os.path.exists(punch_time_path): + LOG.debug(_("Creating punch time folder %s") % punch_time_path) + os.makedirs(punch_time_path) + return punch_time_path + + def get_spawn_folder(self): + spawn_folder = os.path.join(self._get_image_tmp_path(), "spawn_tmp") + if not os.path.exists(spawn_folder): + LOG.debug(_("Creating the spawn folder %s") % spawn_folder) + os.makedirs(spawn_folder) + return spawn_folder + + def make_time_stamp(self): + tmp_file_fn = time.strftime('%Y%m%d%H%M%S', + time.localtime(time.time())) + return tmp_file_fn + + def get_snapshot_time_path(self): + snapshot_time_path = os.path.join(self._get_snapshot_path(), + self.make_time_stamp()) + if not os.path.exists(snapshot_time_path): + LOG.debug(_('Creating folder %s for image bundle temp file') % + snapshot_time_path) + os.makedirs(snapshot_time_path) + return snapshot_time_path + + def clean_temp_folder(self, tmp_file_fn): + if os.path.isdir(tmp_file_fn): + LOG.debug(_('Removing existing folder %s '), tmp_file_fn) + shutil.rmtree(tmp_file_fn) + + def _get_instances_path(self): + return os.path.normpath(CONF.instances_path) + + def get_instance_path(self, os_node, instance_name): + instance_folder = os.path.join(self._get_instances_path(), os_node, + instance_name) + if not os.path.exists(instance_folder): + LOG.debug(_("Creating the instance path %s") % instance_folder) + os.makedirs(instance_folder) + return instance_folder + + def get_console_log_path(self, os_node, instance_name): + return os.path.join(self.get_instance_path(os_node, instance_name), + "console.log") + + +class NetworkUtils(object): + """Utilities for z/VM network operator.""" + + def validate_ip_address(self, ip_address): + """Check whether ip_address is valid.""" + # TODO(Leon): check IP address format + pass + + def validate_network_mask(self, mask): + """Check whether mask is valid.""" + # TODO(Leon): check network mask format + pass + + def create_network_configuration_files(self, file_path, network_info, + base_vdev, os_type): + """Generate network configuration files to instance.""" + device_num = 0 + cfg_files = [] + cmd_strings = '' + udev_cfg_str = '' + dns_cfg_str = '' + route_cfg_str = '' + cmd_str = None + cfg_str = '' + # Red Hat + file_path_rhel = '/etc/sysconfig/network-scripts/' + # SuSE + file_path_sles = '/etc/sysconfig/network/' + file_name_route = file_path_sles + 'routes' + + # Check the OS type + if (os_type == 'sles'): + file_path = file_path_sles + else: + file_path = file_path_rhel + file_name_dns = '/etc/resolv.conf' + for vif in network_info: + file_name = 'ifcfg-eth' + str(device_num) + network = vif['network'] + (cfg_str, cmd_str, dns_str, route_str) =\ + self.generate_network_configration(network, + base_vdev, device_num, os_type) + LOG.debug(_('Network configure file content is: %s') % cfg_str) + target_net_conf_file_name = file_path + file_name + cfg_files.append((target_net_conf_file_name, cfg_str)) + udev_cfg_str += self.generate_udev_configuration(device_num, + '0.0.' + str(base_vdev).zfill(4)) + if cmd_str is not None: + cmd_strings += cmd_str + if len(dns_str) > 0: + dns_cfg_str += dns_str + if len(route_str) > 0: + route_cfg_str += route_str + base_vdev = str(hex(int(base_vdev, 16) + 3))[2:] + device_num += 1 + + if len(dns_cfg_str) > 0: + cfg_files.append((file_name_dns, dns_cfg_str)) + if os_type == 'sles': + udev_file_name = '/etc/udev/rules.d/70-persistent-net.rules' + cfg_files.append((udev_file_name, udev_cfg_str)) + if len(route_cfg_str) > 0: + cfg_files.append((file_name_route, route_cfg_str)) + + return cfg_files, cmd_strings + + def generate_network_configration(self, network, vdev, device_num, + os_type): + """Generate network configuration items.""" + ip_v4 = netmask_v4 = gateway_v4 = broadcast_v4 = '' + subchannels = None + device = None + cidr_v4 = None + cmd_str = None + dns_str = '' + route_str = '' + + subnets_v4 = [s for s in network['subnets'] if s['version'] == 4] + + if len(subnets_v4[0]['ips']) > 0: + ip_v4 = subnets_v4[0]['ips'][0]['address'] + if len(subnets_v4[0]['dns']) > 0: + for dns in subnets_v4[0]['dns']: + dns_str += 'nameserver ' + dns['address'] + '\n' + + netmask_v4 = str(subnets_v4[0].as_netaddr().netmask) + gateway_v4 = subnets_v4[0]['gateway']['address'] + broadcast_v4 = str(subnets_v4[0].as_netaddr().broadcast) + device = "eth" + str(device_num) + address_read = str(vdev).zfill(4) + address_write = str(hex(int(vdev, 16) + 1))[2:].zfill(4) + address_data = str(hex(int(vdev, 16) + 2))[2:].zfill(4) + subchannels = '0.0.%s' % address_read.lower() + subchannels += ',0.0.%s' % address_write.lower() + subchannels += ',0.0.%s' % address_data.lower() + + cfg_str = 'DEVICE=\"' + device + '\"\n' + 'BOOTPROTO=\"static\"\n' + cfg_str += 'BROADCAST=\"' + broadcast_v4 + '\"\n' + cfg_str += 'GATEWAY=\"' + gateway_v4 + '\"\nIPADDR=\"' + ip_v4 + '\"\n' + cfg_str += 'NETMASK=\"' + netmask_v4 + '\"\n' + cfg_str += 'NETTYPE=\"qeth\"\nONBOOT=\"yes\"\n' + cfg_str += 'PORTNAME=\"PORT' + address_read + '\"\n' + cfg_str += 'OPTIONS=\"layer2=1\"\n' + cfg_str += 'SUBCHANNELS=\"' + subchannels + '\"\n' + + if os_type == 'sles': + cidr_v4 = self._get_cidr_from_ip_netmask(ip_v4, netmask_v4) + cmd_str = 'qeth_configure -l 0.0.%s ' % address_read.lower() + cmd_str += '0.0.%(write)s 0.0.%(data)s 1\n' % {'write': + address_write.lower(), 'data': address_data.lower()} + cfg_str = "BOOTPROTO=\'static\'\nIPADDR=\'%s\'\n" % cidr_v4 + cfg_str += "BROADCAST=\'%s\'\n" % broadcast_v4 + cfg_str += "STARTMODE=\'onboot\'\n" + cfg_str += "NAME=\'OSA Express Network card (%s)\'\n" %\ + address_read + route_str += 'default %s - -\n' % gateway_v4 + + return cfg_str, cmd_str, dns_str, route_str + + def generate_udev_configuration(self, device, dev_channel): + cfg_str = 'SUBSYSTEM==\"net\", ACTION==\"add\", DRIVERS==\"qeth\",' + cfg_str += ' KERNELS==\"%s\", ATTR{type}==\"1\",' % dev_channel + cfg_str += ' KERNEL==\"eth*\", NAME=\"eth%s\"\n' % device + + return cfg_str + + def _get_cidr_from_ip_netmask(self, ip, netmask): + netmask_fields = netmask.split('.') + bin_str = '' + for octet in netmask_fields: + bin_str += bin(int(octet))[2:].zfill(8) + mask = str(len(bin_str.rstrip('0'))) + cidr_v4 = ip + '/' + mask + return cidr_v4 diff --git a/nova-zvm-virt-driver/nova/virt/zvm/vif.py b/nova-zvm-virt-driver/nova/virt/zvm/vif.py new file mode 100755 index 0000000..4d73938 --- /dev/null +++ b/nova-zvm-virt-driver/nova/virt/zvm/vif.py @@ -0,0 +1,45 @@ +# vim: tabstop=4 shiftwidth=4 softtabstop=4 + +# Copyright 2013 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 abc + +from nova.openstack.common import log as logging + + +LOG = logging.getLogger(__name__) + + +class ZVMBaseVIFDriver(object): + @abc.abstractmethod + def plug(self, instance, vif): + pass + + @abc.abstractmethod + def unplug(self, instance, vif): + pass + + +class ZVMNeutronVIFDriver(ZVMBaseVIFDriver): + """Neutron VIF driver.""" + + def plug(self, instance, vif): + # Neutron takes care of plugging the port + pass + + def unplug(self, instance, vif): + # Neutron takes care of unplugging the port + pass diff --git a/nova-zvm-virt-driver/nova/virt/zvm/volumeop.py b/nova-zvm-virt-driver/nova/virt/zvm/volumeop.py new file mode 100755 index 0000000..54cbf8b --- /dev/null +++ b/nova-zvm-virt-driver/nova/virt/zvm/volumeop.py @@ -0,0 +1,774 @@ +# Copyright 2013 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 os +from random import randint +import re +import time + +from oslo.config import cfg + +import nova.context +from nova.objects import block_device as block_device_obj +from nova.objects import instance as instance_obj +from nova.openstack.common.gettextutils import _ +from nova.openstack.common import jsonutils +from nova.openstack.common import log as logging +from nova.virt.zvm import exception +from nova.virt.zvm import utils as zvmutils +from nova import volume + + +LOG = logging.getLogger(__name__) +CONF = cfg.CONF + + +class VolumeOperator(object): + """Volume operator on IBM z/VM platform.""" + + def __init__(self): + self._svc_driver = SVCDriver() + + def init_host(self, host_stats): + try: + self._svc_driver.init_host(host_stats) + except (exception.ZVMDriverError, exception.ZVMVolumeError) as err: + LOG.warning(_("Initialize zhcp failed. Reason: %s") % err) + + def attach_volume_to_instance(self, context, connection_info, instance, + mountpoint, is_active, rollback=True): + """Attach a volume to an instance.""" + + if None in [connection_info, instance, is_active]: + errmsg = _("Missing required parameters.") + raise exception.ZVMDriverError(msg=errmsg) + + LOG.debug(_("Attach a volume to an instance. conn_info: %(info)s; " + + "instance: %(name)s; mountpoint: %(point)s") % + {'info': connection_info, 'name': instance['name'], + 'point': mountpoint}) + + if is_active: + self._svc_driver.attach_volume_active(context, connection_info, + instance, mountpoint, + rollback) + else: + self._svc_driver.attach_volume_inactive(context, connection_info, + instance, mountpoint, + rollback) + + def detach_volume_from_instance(self, connection_info, instance, + mountpoint, is_active, rollback=True): + """Detach a volume from an instance.""" + + if None in [connection_info, instance, is_active]: + errmsg = _("Missing required parameters.") + raise exception.ZVMDriverError(msg=errmsg) + + LOG.debug(_("Detach a volume from an instance. conn_info: %(info)s; " + + "instance: %(name)s; mountpoint: %(point)s") % + {'info': connection_info, 'name': instance['name'], + 'point': mountpoint}) + + if is_active: + self._svc_driver.detach_volume_active(connection_info, instance, + mountpoint, rollback) + else: + self._svc_driver.detach_volume_inactive(connection_info, instance, + mountpoint, rollback) + + def get_volume_connector(self, instance): + if not instance: + errmsg = _("Instance must be provided.") + raise exception.ZVMDriverError(msg=errmsg) + return self._svc_driver.get_volume_connector(instance) + + def has_persistent_volume(self, instance): + if not instance: + errmsg = _("Instance must be provided.") + raise exception.ZVMDriverError(msg=errmsg) + return self._svc_driver.has_persistent_volume(instance) + + +@contextlib.contextmanager +def wrap_internal_errors(): + """Wrap internal exceptions to ZVMVolumeError.""" + + try: + yield + except exception.ZVMBaseException: + raise + except Exception as err: + raise exception.ZVMVolumeError(msg=err) + + +class DriverAPI(object): + """DriverAPI for implement volume_attach on IBM z/VM platform.""" + + def init_host(self, host_stats): + """Initialize host environment.""" + raise NotImplementedError + + def get_volume_connector(self, instance): + """Get volume connector for current driver.""" + raise NotImplementedError + + def attach_volume_active(self, context, connection_info, instance, + mountpoint, rollback): + """Attach a volume to an running instance.""" + raise NotImplementedError + + def detach_volume_active(self, connection_info, instance, mountpoint, + rollback): + """Detach a volume from an running instance.""" + raise NotImplementedError + + def attach_volume_inactive(self, context, connection_info, instance, + mountpoint, rollback): + """Attach a volume to an shutdown instance.""" + raise NotImplementedError + + def detach_volume_inactive(self, connection_info, instance, mountpoint, + rollback): + """Detach a volume from an shutdown instance.""" + raise NotImplementedError + + def has_persistent_volume(self, instance): + """Decide if the specified instance has persistent volumes attached.""" + raise NotImplementedError + + +class SVCDriver(DriverAPI): + """SVC volume operator on IBM z/VM platform.""" + + def __init__(self): + self._xcat_url = zvmutils.XCATUrl() + self._path_utils = zvmutils.PathUtils() + self._host = CONF.zvm_host + self._pool_name = CONF.zvm_scsi_pool + self._fcp_pool = set() + self._instance_fcp_map = {} + self._is_instance_fcp_map_locked = False + self._volume_api = volume.API() + + self._actions = {'attach_volume': 'addScsiVolume', + 'detach_volume': 'removeScsiVolume', + 'create_mountpoint': 'createfilesysnode', + 'remove_mountpoint': 'removefilesysnode'} + + self._RESERVE = 0 + self._INCREASE = 1 + self._DECREASE = 2 + self._REMOVE = 3 + + def init_host(self, host_stats): + """Initialize host environment.""" + + if not host_stats: + errmsg = _("Can not obtain host stats.") + raise exception.ZVMDriverError(msg=errmsg) + + zhcp_fcp_list = CONF.zvm_zhcp_fcp_list + fcp_devices = self._expand_fcp_list(zhcp_fcp_list) + hcpnode = host_stats[0]['zhcp']['nodename'] + for _fcp in fcp_devices: + with zvmutils.ignore_errors(): + self._attach_device(hcpnode, _fcp) + with zvmutils.ignore_errors(): + self._online_device(hcpnode, _fcp) + + fcp_list = CONF.zvm_fcp_list + if (fcp_list is None): + errmsg = _("At least one fcp list should be given") + LOG.error(errmsg) + raise exception.ZVMVolumeError(msg=errmsg) + self._init_fcp_pool(fcp_list) + + def _init_fcp_pool(self, fcp_list): + """Map all instances and their fcp devices, and record all free fcps. + One instance should use only one fcp device so far. + """ + + self._fcp_pool = self._expand_fcp_list(fcp_list) + self._instance_fcp_map = {} + # Any other functions should not modify _instance_fcp_map during + # FCP pool initialization + self._is_instance_fcp_map_locked = True + + compute_host_bdms = self._get_host_volume_bdms() + for instance_bdms in compute_host_bdms: + instance_name = instance_bdms['instance']['name'] + + for _bdm in instance_bdms['instance_bdms']: + connection_info = self._build_connection_info(_bdm) + try: + _fcp = connection_info['data']['zvm_fcp'] + if _fcp and _fcp in self._fcp_pool: + self._update_instance_fcp_map(instance_name, _fcp, + self._INCREASE) + if _fcp and _fcp not in self._fcp_pool: + errmsg = _("FCP device %s is not configured but " + + "is used by %s.") % (_fcp, instance_name) + LOG.warning(errmsg) + except (TypeError, KeyError): + pass + + for _key in self._instance_fcp_map.keys(): + fcp = self._instance_fcp_map.get(_key)['fcp'] + self._fcp_pool.remove(fcp) + self._is_instance_fcp_map_locked = False + + def _update_instance_fcp_map_if_unlocked(self, instance_name, fcp, action): + while self._is_instance_fcp_map_locked: + time.sleep(1) + self._update_instance_fcp_map(instance_name, fcp, action) + + def _update_instance_fcp_map(self, instance_name, fcp, action): + fcp = fcp.lower() + if instance_name in self._instance_fcp_map: + # One instance should use only one fcp device so far + current_fcp = self._instance_fcp_map.get(instance_name)['fcp'] + if fcp != current_fcp: + errmsg = _("Instance %(ins_name)s has multiple FCP devices " + + "attached! FCP1: %(fcp1)s, FCP2: %(fcp2)s" + ) % {'ins_name': instance_name, 'fcp1': fcp, + 'fcp2': current_fcp} + LOG.warning(errmsg) + return + + if action == self._RESERVE: + if instance_name in self._instance_fcp_map: + count = self._instance_fcp_map[instance_name]['count'] + if count > 0: + errmsg = _("Try to reserve a fcp device which already " + + "has volumes attached on: %(ins_name)s:%(fcp)s" + ) % {'ins_name': instance_name, 'fcp': fcp} + LOG.warning(errmsg) + else: + new_item = {instance_name: {'fcp': fcp, 'count': 0}} + self._instance_fcp_map.update(new_item) + + elif action == self._INCREASE: + if instance_name in self._instance_fcp_map: + count = self._instance_fcp_map[instance_name]['count'] + new_item = {instance_name: {'fcp': fcp, 'count': count + 1}} + self._instance_fcp_map.update(new_item) + else: + new_item = {instance_name: {'fcp': fcp, 'count': 1}} + self._instance_fcp_map.update(new_item) + + elif action == self._DECREASE: + if instance_name in self._instance_fcp_map: + count = self._instance_fcp_map[instance_name]['count'] + if count > 0: + new_item = {instance_name: {'fcp': fcp, + 'count': count - 1}} + self._instance_fcp_map.update(new_item) + else: + fcp = self._instance_fcp_map[instance_name]['fcp'] + self._instance_fcp_map.pop(instance_name) + self._fcp_pool.add(fcp) + else: + errmsg = _("Try to decrease an inexistent map item: " + + "%(ins_name)s:%(fcp)s" + ) % {'ins_name': instance_name, 'fcp': fcp} + LOG.warning(errmsg) + + elif action == self._REMOVE: + if instance_name in self._instance_fcp_map: + count = self._instance_fcp_map[instance_name]['count'] + if count > 0: + errmsg = _("Try to remove a map item while some volumes " + + "are still attached on: %(ins_name)s:%(fcp)s" + ) % {'ins_name': instance_name, 'fcp': fcp} + LOG.warning(errmsg) + else: + fcp = self._instance_fcp_map[instance_name]['fcp'] + self._instance_fcp_map.pop(instance_name) + self._fcp_pool.add(fcp) + else: + errmsg = _("Try to remove an inexistent map item: " + + "%(ins_name)s:%(fcp)s" + ) % {'ins_name': instance_name, 'fcp': fcp} + LOG.warning(errmsg) + + else: + errmsg = _("Unrecognized option: %s") % action + LOG.warning(errmsg) + + def _get_host_volume_bdms(self): + """Return all block device mappings on a compute host.""" + + compute_host_bdms = [] + instances = self._get_all_instances() + for instance in instances: + instance_bdms = self._get_instance_bdms(instance) + compute_host_bdms.append(dict(instance=instance, + instance_bdms=instance_bdms)) + + return compute_host_bdms + + def _get_all_instances(self): + context = nova.context.get_admin_context() + return instance_obj.InstanceList.get_by_host(context, self._host) + + def _get_instance_bdms(self, instance): + context = nova.context.get_admin_context() + instance_bdms = [bdm for bdm in + (block_device_obj.BlockDeviceMappingList. + get_by_instance_uuid(context, instance['uuid'])) + if bdm.is_volume] + return instance_bdms + + def has_persistent_volume(self, instance): + return bool(self._get_instance_bdms(instance)) + + def _build_connection_info(self, bdm): + try: + connection_info = jsonutils.loads(bdm['connection_info']) + return connection_info + except (TypeError, KeyError, ValueError): + return None + + def get_volume_connector(self, instance): + try: + fcp = self._instance_fcp_map.get(instance['name'])['fcp'] + except Exception: + fcp = None + if not fcp: + fcp = self._get_fcp_from_pool() + if fcp: + self._update_instance_fcp_map_if_unlocked(instance['name'], + fcp, self._RESERVE) + + if not fcp: + errmsg = _("No available FCP device found.") + LOG.error(errmsg) + raise exception.ZVMVolumeError(msg=errmsg) + fcp = fcp.lower() + + wwpn = self._get_wwpn(fcp) + if not wwpn: + errmsg = _("FCP device %s has no available WWPN.") % fcp + LOG.error(errmsg) + raise exception.ZVMVolumeError(msg=errmsg) + wwpn = wwpn.lower() + + return {'zvm_fcp': fcp, 'wwpns': [wwpn], 'host': CONF.zvm_host} + + def _get_wwpn(self, fcp): + states = ['active', 'free'] + for _state in states: + fcps_info = self._list_fcp_details(_state) + if not fcps_info: + continue + wwpn = self._extract_wwpn_from_fcp_info(fcp, fcps_info) + if wwpn: + return wwpn + + def _list_fcp_details(self, state): + fields = '&field=--fcpdevices&field=' + state + '&field=details' + rsp = self._xcat_rinv(fields) + try: + fcp_details = rsp['info'][0][0].splitlines() + return fcp_details + except (TypeError, KeyError): + return None + + def _extract_wwpn_from_fcp_info(self, fcp, fcps_info): + """The FCP infomation would look like this: + host: FCP device number: xxxx + host: Status: Active + host: NPIV world wide port number: xxxxxxxx + host: Channel path ID: xx + host: Physical world wide port number: xxxxxxxx + ...... + host: FCP device number: xxxx + host: Status: Active + host: NPIV world wide port number: xxxxxxxx + host: Channel path ID: xx + host: Physical world wide port number: xxxxxxxx + + """ + + lines_per_item = 5 + num_fcps = len(fcps_info) / lines_per_item + fcp = fcp.upper() + for _cur in range(0, num_fcps): + # Find target FCP device + if fcp not in fcps_info[_cur * lines_per_item]: + continue + # Try to get NPIV WWPN first + wwpn_info = fcps_info[(_cur + 1) * lines_per_item - 3] + wwpn = self._get_wwpn_from_line(wwpn_info) + if not wwpn: + # Get physical WWPN if NPIV WWPN is none + wwpn_info = fcps_info[(_cur + 1) * lines_per_item - 1] + wwpn = self._get_wwpn_from_line(wwpn_info) + return wwpn + + def _get_wwpn_from_line(self, info_line): + wwpn = info_line.split(':')[-1].strip() + if wwpn and wwpn.upper() != 'NONE': + return wwpn + else: + return None + + def _get_fcp_from_pool(self): + if self._fcp_pool: + return self._fcp_pool.pop() + + self._init_fcp_pool(CONF.zvm_fcp_list) + if self._fcp_pool: + return self._fcp_pool.pop() + + def _extract_connection_info(self, context, connection_info): + with wrap_internal_errors(): + LOG.debug(_("Extract connection_info: %s") % connection_info) + + lun = connection_info['data']['target_lun'] + lun = "%04x000000000000" % int(lun) + wwpn = connection_info['data']['target_wwn'] + size = '0G' + # There is no context in detach case + if context: + volume_id = connection_info['data']['volume_id'] + volume = self._get_volume_by_id(context, volume_id) + size = str(volume['size']) + 'G' + fcp = connection_info['data']['zvm_fcp'] + + return (lun.lower(), wwpn.lower(), size, fcp.lower()) + + def _get_volume_by_id(self, context, volume_id): + volume = self._volume_api.get(context, volume_id) + return volume + + def attach_volume_active(self, context, connection_info, instance, + mountpoint, rollback=True): + """Attach a volume to an running instance.""" + + (lun, wwpn, size, fcp) = self._extract_connection_info(context, + connection_info) + try: + self._update_instance_fcp_map_if_unlocked(instance['name'], fcp, + self._INCREASE) + self._add_zfcp_to_pool(fcp, wwpn, lun, size) + self._add_zfcp(instance, fcp, wwpn, lun, size) + if mountpoint: + self._create_mountpoint(instance, fcp, wwpn, lun, mountpoint) + except (exception.ZVMXCATRequestFailed, + exception.ZVMInvalidXCATResponseDataError, + exception.ZVMXCATInternalError, + exception.ZVMVolumeError): + self._update_instance_fcp_map_if_unlocked(instance['name'], fcp, + self._DECREASE) + do_detach = not self._is_fcp_in_use(instance, fcp) + if rollback: + with zvmutils.ignore_errors(): + self._remove_mountpoint(instance, mountpoint) + with zvmutils.ignore_errors(): + self._remove_zfcp(instance, fcp, wwpn, lun) + with zvmutils.ignore_errors(): + self._remove_zfcp_from_pool(wwpn, lun) + with zvmutils.ignore_errors(): + if do_detach: + self._detach_device(instance['name'], fcp) + raise + + def detach_volume_active(self, connection_info, instance, mountpoint, + rollback=True): + """Detach a volume from an running instance.""" + + (lun, wwpn, size, fcp) = self._extract_connection_info(None, + connection_info) + try: + self._update_instance_fcp_map_if_unlocked(instance['name'], fcp, + self._DECREASE) + do_detach = not self._is_fcp_in_use(instance, fcp) + if mountpoint: + self._remove_mountpoint(instance, mountpoint) + self._remove_zfcp(instance, fcp, wwpn, lun) + self._remove_zfcp_from_pool(wwpn, lun) + if do_detach: + self._detach_device(instance['name'], fcp) + self._update_instance_fcp_map_if_unlocked(instance['name'], + fcp, self._REMOVE) + except (exception.ZVMXCATRequestFailed, + exception.ZVMInvalidXCATResponseDataError, + exception.ZVMXCATInternalError, + exception.ZVMVolumeError): + self._update_instance_fcp_map_if_unlocked(instance['name'], fcp, + self._INCREASE) + if rollback: + with zvmutils.ignore_errors(): + self._add_zfcp_to_pool(fcp, wwpn, lun, size) + with zvmutils.ignore_errors(): + self._add_zfcp(instance, fcp, wwpn, lun, size) + if mountpoint: + with zvmutils.ignore_errors(): + self._create_mountpoint(instance, fcp, wwpn, lun, + mountpoint) + raise + + def attach_volume_inactive(self, context, connection_info, instance, + mountpoint, rollback=True): + """Attach a volume to an shutdown instance.""" + + (lun, wwpn, size, fcp) = self._extract_connection_info(context, + connection_info) + try: + do_attach = not self._is_fcp_in_use(instance, fcp) + self._update_instance_fcp_map_if_unlocked(instance['name'], fcp, + self._INCREASE) + self._add_zfcp_to_pool(fcp, wwpn, lun, size) + self._allocate_zfcp(instance, fcp, size, wwpn, lun) + self._notice_attach(instance, fcp, wwpn, lun, mountpoint) + if do_attach: + self._attach_device(instance['name'], fcp) + except (exception.ZVMXCATRequestFailed, + exception.ZVMInvalidXCATResponseDataError, + exception.ZVMXCATInternalError, + exception.ZVMVolumeError): + self._update_instance_fcp_map_if_unlocked(instance['name'], fcp, + self._DECREASE) + do_detach = not self._is_fcp_in_use(instance, fcp) + if rollback: + with zvmutils.ignore_errors(): + self._notice_detach(instance, fcp, wwpn, lun, mountpoint) + with zvmutils.ignore_errors(): + self._remove_zfcp_from_pool(wwpn, lun) + with zvmutils.ignore_errors(): + if do_detach: + self._detach_device(instance['name'], fcp) + self._update_instance_fcp_map_if_unlocked( + instance['name'], fcp, self._REMOVE) + raise + + def detach_volume_inactive(self, connection_info, instance, mountpoint, + rollback=True): + """Detach a volume from an shutdown instance.""" + + (lun, wwpn, size, fcp) = self._extract_connection_info(None, + connection_info) + try: + self._update_instance_fcp_map_if_unlocked(instance['name'], fcp, + self._DECREASE) + do_detach = not self._is_fcp_in_use(instance, fcp) + self._remove_zfcp(instance, fcp, wwpn, lun) + self._remove_zfcp_from_pool(wwpn, lun) + self._notice_detach(instance, fcp, wwpn, lun, mountpoint) + if do_detach: + self._detach_device(instance['name'], fcp) + self._update_instance_fcp_map_if_unlocked(instance['name'], + fcp, self._REMOVE) + except (exception.ZVMXCATRequestFailed, + exception.ZVMInvalidXCATResponseDataError, + exception.ZVMXCATInternalError, + exception.ZVMVolumeError): + self._update_instance_fcp_map_if_unlocked(instance['name'], fcp, + self._INCREASE) + if rollback: + with zvmutils.ignore_errors(): + self._attach_device(instance['name'], fcp) + with zvmutils.ignore_errors(): + self._notice_attach(instance, fcp, wwpn, lun, mountpoint) + with zvmutils.ignore_errors(): + self._add_zfcp_to_pool(fcp, wwpn, lun, size) + with zvmutils.ignore_errors(): + self._allocate_zfcp(instance, fcp, size, wwpn, lun) + raise + + def _expand_fcp_list(self, fcp_list): + """Expand fcp list string into a python list object which contains + each fcp devices in the list string. A fcp list is composed of fcp + device addresses, range indicator '-', and split indicator ';'. + + For example, if fcp_list is + "0011-0013;0015;0017-0018", expand_fcp_list(fcp_list) will return + [0011, 0012, 0013, 0015, 0017, 0018]. + + """ + + LOG.debug(_("Expand FCP list %s") % fcp_list) + + if not fcp_list: + return set() + + range_pattern = '[0-9a-fA-F]{1,4}(-[0-9a-fA-F]{1,4})?' + match_pattern = "^(%(range)s)(;%(range)s)*$" % {'range': range_pattern} + if not re.match(match_pattern, fcp_list): + errmsg = _("Invalid FCP address %s") % fcp_list + raise exception.ZVMDriverError(msg=errmsg) + + fcp_devices = set() + for _range in fcp_list.split(';'): + if '-' not in _range: + # single device + fcp_addr = int(_range, 16) + fcp_devices.add("%04x" % fcp_addr) + else: + # a range of address + (_min, _max) = _range.split('-') + _min = int(_min, 16) + _max = int(_max, 16) + for fcp_addr in range(_min, _max + 1): + fcp_devices.add("%04x" % fcp_addr) + + # remove duplicate entries + return fcp_devices + + def _attach_device(self, node, addr, mode='0'): + """Attach a device to a node.""" + + body = [' '.join(['--dedicatedevice', addr, addr, mode])] + self._xcat_chvm(node, body) + + def _detach_device(self, node, vdev): + """Detach a device from a node.""" + + body = [' '.join(['--undedicatedevice', vdev])] + self._xcat_chvm(node, body) + + def _online_device(self, node, dev): + """After attaching a device to a node, the device should be made + online before it being in use. + + """ + + body = ["command=cio_ignore -r %s" % dev] + self._xcat_xdsh(node, body) + + body = ["command=chccwdev -e %s" % dev] + self._xcat_xdsh(node, body) + + def _is_fcp_in_use(self, instance, fcp): + if instance['name'] in self._instance_fcp_map: + count = self._instance_fcp_map.get(instance['name'])['count'] + if count > 0: + return True + return False + + def _notice_attach(self, instance, fcp, wwpn, lun, mountpoint): + file_path = self._get_file_for_punch(instance) + + # Create and send volume file + action = self._actions['attach_volume'] + lines = self._get_volume_lines(action, fcp, wwpn, lun) + self._send_notice(instance, file_path, lines) + + # Create and send mount point file + action = self._actions['create_mountpoint'] + lines = self._get_mountpoint_lines(action, fcp, wwpn, lun, mountpoint) + self._send_notice(instance, file_path, lines) + + def _notice_detach(self, instance, fcp, wwpn, lun, mountpoint): + file_path = self._get_file_for_punch(instance) + + # Create and send volume file + action = self._actions['detach_volume'] + lines = self._get_volume_lines(action, fcp, wwpn, lun) + self._send_notice(instance, file_path, lines) + + # Create and send mount point file + action = self._actions['remove_mountpoint'] + lines = self._get_mountpoint_lines(action, fcp, wwpn, lun, mountpoint) + self._send_notice(instance, file_path, lines) + + def _get_volume_lines(self, action, fcp, wwpn, lun): + comments = '# xCAT Init\n' + action_line = "action=%s\n" % action + fcp_line = "fcpAddr=%s\n" % fcp + wwpn_line = "wwpn=%s\n" % wwpn + lun_line = "lun=%s\n" % lun + return [comments, action_line, fcp_line, wwpn_line, lun_line] + + def _get_mountpoint_lines(self, action, fcp, wwpn, lun, mountpoint): + comments = '# xCAT Init\n' + action_line = "action=%s\n" % action + mountpoint_line = "tgtFile=%s\n" % mountpoint + if action == self._actions['create_mountpoint']: + path = self._get_zfcp_path_pattern() + srcdev = path % {'fcp': fcp, 'wwpn': wwpn, 'lun': lun} + srcdev_line = "srcFile=%s\n" % srcdev + return [comments, action_line, mountpoint_line, srcdev_line] + else: + return [comments, action_line, mountpoint_line] + + def _get_file_for_punch(self, instance): + dir_path = self._path_utils.get_punch_time_path() + file_name = "%08x.disk" % randint(0, int('FFFFFFFF', 16)) + file_path = os.path.join(dir_path, file_name) + return file_path + + def _send_notice(self, instance, file_path, lines): + with wrap_internal_errors(): + with open(file_path, 'w') as f: + f.writelines(lines) + + # zvmutils.punch_file will remove the file after punching + zvmutils.punch_file(instance['name'], file_path, 'X') + + def _add_zfcp_to_pool(self, fcp, wwpn, lun, size): + body = [' '.join(['--addzfcp2pool', self._pool_name, 'free', wwpn, + lun, size, fcp])] + self._xcat_chhy(body) + + def _remove_zfcp_from_pool(self, wwpn, lun): + body = [' '.join(['--removezfcpfrompool', CONF.zvm_scsi_pool, lun, + wwpn])] + self._xcat_chhy(body) + + def _add_zfcp(self, instance, fcp, wwpn, lun, size): + body = [' '.join(['--addzfcp', CONF.zvm_scsi_pool, fcp, str(0), size, + str(0), wwpn, lun])] + self._xcat_chvm(instance['name'], body) + + def _remove_zfcp(self, instance, fcp, wwpn, lun): + body = [' '.join(['--removezfcp', fcp, wwpn, lun, '1'])] + self._xcat_chvm(instance['name'], body) + + def _create_mountpoint(self, instance, fcp, wwpn, lun, mountpoint): + path = self._get_zfcp_path_pattern() + srcdev = path % {'fcp': fcp, 'wwpn': wwpn, 'lun': lun} + body = [" ".join(['--createfilesysnode', srcdev, mountpoint])] + self._xcat_chvm(instance['name'], body) + + def _remove_mountpoint(self, instance, mountpoint): + body = [' '.join(['--removefilesysnode', mountpoint])] + self._xcat_chvm(instance['name'], body) + + def _allocate_zfcp(self, instance, fcp, size, wwpn, lun): + body = [" ".join(['--reservezfcp', CONF.zvm_scsi_pool, 'used', + instance['name'], fcp, size, wwpn, lun])] + self._xcat_chhy(body) + + def _xcat_chvm(self, node, body): + url = self._xcat_url.chvm('/' + node) + zvmutils.xcat_request('PUT', url, body) + + def _xcat_chhy(self, body): + url = self._xcat_url.chhv('/' + self._host) + zvmutils.xcat_request('PUT', url, body) + + def _xcat_xdsh(self, node, body): + url = self._xcat_url.xdsh('/' + node) + zvmutils.xcat_request('PUT', url, body) + + def _xcat_rinv(self, fields): + url = self._xcat_url.rinv('/' + self._host, fields) + return zvmutils.xcat_request('GET', url) + + def _get_zfcp_path_pattern(self): + return '/dev/disk/by-path/ccw-0.0.%(fcp)s-zfcp-0x%(wwpn)s:0x%(lun)s'