├── .gitignore ├── .gitreview ├── README.rst ├── neutron-zvm-plugin └── neutron │ ├── plugins │ └── zvm │ │ ├── __init__.py │ │ ├── agent │ │ ├── __init__.py │ │ ├── zvm_network.py │ │ └── zvm_neutron_agent.py │ │ └── common │ │ ├── __init__.py │ │ ├── config.py │ │ ├── constants.py │ │ ├── exception.py │ │ ├── utils.py │ │ └── xcatutils.py │ └── tests │ └── unit │ └── zvm │ ├── test_zvm_network.py │ ├── test_zvm_neutron_agent.py │ ├── test_zvm_utils.py │ └── test_zvm_xcatutils.py └── nova-zvm-virt-driver └── nova ├── tests └── test_zvm.py └── virt └── zvm ├── __init__.py ├── configdrive.py ├── const.py ├── driver.py ├── exception.py ├── imageop.py ├── instance.py ├── networkop.py ├── utils.py ├── vif.py └── volumeop.py /.gitignore: -------------------------------------------------------------------------------- 1 | *.egg* 2 | *.log 3 | *.pyc 4 | .project 5 | .pydevproject 6 | .tox 7 | .venv 8 | build/* 9 | dist/* 10 | -------------------------------------------------------------------------------- /.gitreview: -------------------------------------------------------------------------------- 1 | [gerrit] 2 | host=review.openstack.org 3 | port=29418 4 | project=stackforge/zvm-driver.git 5 | -------------------------------------------------------------------------------- /README.rst: -------------------------------------------------------------------------------- 1 | This project contains a set of drivers and plugins for different OpenStack 2 | components that enables OpenStack manage z/VM hypervisor and virtual machines 3 | running in the z/VM system. 4 | 5 | Wiki page: https://wiki.openstack.org/wiki/ZVMDriver 6 | -------------------------------------------------------------------------------- /neutron-zvm-plugin/neutron/plugins/zvm/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stackforge-attic/zvm-driver/cf77385929302987b63723484d3f78968f404e0d/neutron-zvm-plugin/neutron/plugins/zvm/__init__.py -------------------------------------------------------------------------------- /neutron-zvm-plugin/neutron/plugins/zvm/agent/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stackforge-attic/zvm-driver/cf77385929302987b63723484d3f78968f404e0d/neutron-zvm-plugin/neutron/plugins/zvm/agent/__init__.py -------------------------------------------------------------------------------- /neutron-zvm-plugin/neutron/plugins/zvm/agent/zvm_network.py: -------------------------------------------------------------------------------- 1 | # vim: tabstop=4 shiftwidth=4 softtabstop=4 2 | 3 | # Copyright 2014 IBM Corp. 4 | # 5 | # All Rights Reserved. 6 | # 7 | # Licensed under the Apache License, Version 2.0 (the "License"); you may 8 | # not use this file except in compliance with the License. You may obtain 9 | # a copy of the License at 10 | # 11 | # http://www.apache.org/licenses/LICENSE-2.0 12 | # 13 | # Unless required by applicable law or agreed to in writing, software 14 | # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 15 | # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 16 | # License for the specific language governing permissions and limitations 17 | # under the License. 18 | 19 | from oslo.config import cfg 20 | from neutron.openstack.common import log as logging 21 | from neutron.plugins.common import utils as plugin_utils 22 | from neutron.plugins.zvm.common import utils 23 | 24 | LOG = logging.getLogger(__name__) 25 | 26 | vswitch_opts = [ 27 | cfg.StrOpt('rdev_list', 28 | help='RDev list for vswitch uplink port')] 29 | 30 | CONF = cfg.CONF 31 | CONF.import_opt('flat_networks', "neutron.plugins.ml2.drivers.type_flat", 32 | 'ml2_type_flat') 33 | CONF.import_opt('network_vlan_ranges', "neutron.plugins.ml2.drivers.type_vlan", 34 | 'ml2_type_vlan') 35 | 36 | 37 | class zvmVswitch(object): 38 | def __init__(self, zhcp, name, vlan): 39 | self._utils = utils.zvmUtils() 40 | self._utils.add_vswitch(zhcp, name, 41 | eval("CONF." + name + ".rdev_list"), vid=vlan) 42 | self.zhcp = zhcp 43 | 44 | 45 | class zvmNetwork(object): 46 | def __init__(self): 47 | self._zhcp = CONF.AGENT.xcat_zhcp_nodename 48 | self._vsws = [] 49 | self._maps = {} 50 | self._creat_networks() 51 | 52 | def _creat_networks(self): 53 | self._maps = plugin_utils.parse_network_vlan_ranges( 54 | CONF.ml2_type_vlan.network_vlan_ranges 55 | + CONF.ml2_type_flat.flat_networks) 56 | self._vsws = [] 57 | for vsw in self._maps.keys(): 58 | CONF.register_opts(vswitch_opts, vsw) 59 | self._vsws.append(zvmVswitch(self._zhcp, vsw, self._maps[vsw])) 60 | 61 | def get_network_maps(self): 62 | return self._maps 63 | -------------------------------------------------------------------------------- /neutron-zvm-plugin/neutron/plugins/zvm/agent/zvm_neutron_agent.py: -------------------------------------------------------------------------------- 1 | # vim: tabstop=4 shiftwidth=4 softtabstop=4 2 | 3 | # Copyright 2014 IBM Corp. 4 | # 5 | # All Rights Reserved. 6 | # 7 | # Licensed under the Apache License, Version 2.0 (the "License"); you may 8 | # not use this file except in compliance with the License. You may obtain 9 | # a copy of the License at 10 | # 11 | # http://www.apache.org/licenses/LICENSE-2.0 12 | # 13 | # Unless required by applicable law or agreed to in writing, software 14 | # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 15 | # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 16 | # License for the specific language governing permissions and limitations 17 | # under the License. 18 | 19 | import eventlet 20 | import sys 21 | import time 22 | import zvm_network 23 | 24 | from oslo.config import cfg 25 | 26 | from neutron import context 27 | from neutron.agent import rpc as agent_rpc 28 | from neutron.common import config as common_config 29 | from neutron.common import constants as q_const 30 | from neutron.common import rpc as n_rpc 31 | from neutron.common import topics 32 | from neutron.plugins.common import constants as p_const 33 | from neutron.openstack.common import log as logging 34 | from neutron.openstack.common import loopingcall 35 | from neutron.openstack.common.gettextutils import _ 36 | from neutron.plugins.zvm.common import exception 37 | from neutron.plugins.zvm.common import utils 38 | 39 | LOG = logging.getLogger(__name__) 40 | 41 | 42 | def restart_wrapper(func): 43 | def wrapper(*args, **kw): 44 | gen = func(*args, **kw) 45 | gen.next() 46 | return gen 47 | return wrapper 48 | 49 | 50 | class zvmNeutronAgent(n_rpc.RpcCallback): 51 | RPC_API_VERSION = '1.1' 52 | 53 | def __init__(self): 54 | super(zvmNeutronAgent, self).__init__() 55 | self._utils = utils.zvmUtils() 56 | self._polling_interval = cfg.CONF.AGENT.polling_interval 57 | self._zhcp_node = cfg.CONF.AGENT.xcat_zhcp_nodename 58 | self._host = cfg.CONF.AGENT.zvm_host 59 | 60 | zvm_net = zvm_network.zvmNetwork() 61 | self.agent_state = { 62 | 'binary': 'neutron-zvm-agent', 63 | 'host': self._host, 64 | 'topic': q_const.L2_AGENT_TOPIC, 65 | 'configurations': {'vswitch_mappings': zvm_net.get_network_maps()}, 66 | 'agent_type': q_const.AGENT_TYPE_ZVM, 67 | 'start_flag': True} 68 | self._setup_server_rpc() 69 | self._zhcp_userid = self._utils.get_zhcp_userid(self._zhcp_node) 70 | self._restart_handler = self._handle_restart() 71 | 72 | def _setup_server_rpc(self): 73 | self.agent_id = 'zvm_agent_%s' % self._zhcp_node 74 | self.topic = topics.AGENT 75 | self.plugin_rpc = agent_rpc.PluginApi(topics.PLUGIN) 76 | self.state_rpc = agent_rpc.PluginReportStateAPI(topics.PLUGIN) 77 | 78 | self.context = context.get_admin_context_without_session() 79 | 80 | self.endpoints = [self] 81 | consumers = [[topics.PORT, topics.UPDATE], 82 | [topics.NETWORK, topics.DELETE]] 83 | self.connection = agent_rpc.create_consumers(self.endpoints, 84 | self.topic, 85 | consumers) 86 | 87 | report_interval = cfg.CONF.AGENT.report_interval 88 | if report_interval: 89 | heartbeat = loopingcall.FixedIntervalLoopingCall( 90 | self._report_state) 91 | heartbeat.start(interval=report_interval) 92 | 93 | def _report_state(self): 94 | try: 95 | self.state_rpc.report_state(self.context, self.agent_state) 96 | self.agent_state.pop('start_flag', None) 97 | except Exception: 98 | LOG.exception(_("Failed reporting state!")) 99 | 100 | def network_delete(self, context, network_id=None): 101 | LOG.debug(_("Network delete received. UUID: %s"), network_id) 102 | 103 | def port_update(self, context, **kwargs): 104 | port = kwargs.get('port') 105 | LOG.debug(_("Port update received. UUID: %s"), port) 106 | 107 | if not port['id'] in self._port_map.keys(): 108 | # update a port which is not coupled to any NIC, nothing 109 | # to do for a user based vswitch 110 | return 111 | 112 | vswitch = self._port_map[port['id']]['vswitch'] 113 | userid = self._port_map[port['id']]['userid'] 114 | if port['admin_state_up']: 115 | self._utils.couple_nic_to_vswitch(vswitch, port['id'], 116 | self._zhcp_node, userid) 117 | self.plugin_rpc.update_device_up(self.context, port['id'], 118 | self.agent_id) 119 | else: 120 | self._utils.uncouple_nic_from_vswitch(vswitch, port['id'], 121 | self._zhcp_node, userid) 122 | self.plugin_rpc.update_device_down(self.context, port['id'], 123 | self.agent_id) 124 | self._utils.put_user_direct_online(self._zhcp_node, 125 | self._zhcp_userid) 126 | 127 | def port_bound(self, port_id, net_uuid, 128 | network_type, physical_network, segmentation_id, userid): 129 | LOG.debug(_("Binding port %s"), port_id) 130 | 131 | self._utils.grant_user(self._zhcp_node, physical_network, userid) 132 | vdev = self._utils.couple_nic_to_vswitch(physical_network, port_id, 133 | self._zhcp_node, userid) 134 | self._utils.put_user_direct_online(self._zhcp_node, 135 | self._zhcp_userid) 136 | 137 | if network_type == p_const.TYPE_VLAN: 138 | LOG.info(_('Binding VLAN, VLAN ID: %s'), segmentation_id) 139 | self._utils.set_vswitch_port_vlan_id(segmentation_id, port_id, 140 | vdev, self._zhcp_node, 141 | physical_network) 142 | else: 143 | LOG.info(_('Bind %s mode done'), network_type) 144 | 145 | def port_unbound(self, port_id): 146 | LOG.debug(_("Unbinding port %s"), port_id) 147 | # uncouple is not necessary, because revoke user will uncouple it 148 | # automatically. 149 | self._utils.revoke_user(self._zhcp_node, 150 | self._port_map[port_id]['vswitch'], 151 | self._port_map[port_id]['userid']) 152 | 153 | def _update_ports(self, registered_ports): 154 | ports_info = self._utils.get_nic_ids() 155 | ports = set() 156 | for p in ports_info: 157 | target_host = p.split(',')[5].strip('"') 158 | new_port_id = p.split(',')[2].strip('"') 159 | if target_host == self._zhcp_node: 160 | ports.add(new_port_id) 161 | 162 | if ports == registered_ports: 163 | return 164 | 165 | added = ports - registered_ports 166 | removed = registered_ports - ports 167 | return {'current': ports, 'added': added, 'removed': removed} 168 | 169 | def _treat_vif_port(self, port_id, network_id, network_type, 170 | physical_network, segmentation_id, 171 | admin_state_up): 172 | node = self._utils.get_node_from_port(port_id) 173 | userid = self._utils.get_userid_from_node(node) 174 | LOG.info(_("Update port for node:%s") % node) 175 | if admin_state_up: 176 | self.port_bound(port_id, network_id, network_type, 177 | physical_network, segmentation_id, 178 | userid) 179 | else: 180 | self._utils.grant_user(self._zhcp_node, physical_network, userid) 181 | return (node, userid) 182 | 183 | def _treat_devices_added(self, devices): 184 | for device in devices: 185 | LOG.info(_("Adding port %s") % device) 186 | try: 187 | details = self.plugin_rpc.get_device_details(self.context, 188 | device, 189 | self.agent_id) 190 | except Exception: 191 | LOG.debug(_("Unable to get port details for %s:"), device) 192 | continue 193 | 194 | try: 195 | if 'port_id' in details: 196 | LOG.info(_("Port %(device)s updated." 197 | " Details: %(details)s"), 198 | {'device': device, 'details': details}) 199 | (node, userid) = self._treat_vif_port( 200 | details['port_id'], 201 | details['network_id'], 202 | details['network_type'], 203 | details['physical_network'], 204 | details['segmentation_id'], 205 | details['admin_state_up']) 206 | # add device done, keep port map info 207 | self._port_map[device] = {} 208 | self._port_map[device]['userid'] = userid 209 | self._port_map[device]['nodename'] = node 210 | self._port_map[device]['vswitch'] = details[ 211 | 'physical_network'] 212 | self._port_map[device]['vlan_id'] = details[ 213 | 'segmentation_id'] 214 | 215 | # no rollback if this fails 216 | self._utils.update_xcat_switch(details['port_id'], 217 | details['physical_network'], 218 | details['segmentation_id']) 219 | if details.get('admin_state_up'): 220 | LOG.debug(_("Setting status for %s to UP"), device) 221 | self.plugin_rpc.update_device_up( 222 | self.context, device, self.agent_id, cfg.CONF.host) 223 | else: 224 | LOG.debug(_("Setting status for %s to DOWN"), device) 225 | self.plugin_rpc.update_device_down( 226 | self.context, device, self.agent_id, cfg.CONF.host) 227 | 228 | else: 229 | LOG.debug(_("Device %s not defined on Neutron server"), 230 | device) 231 | continue 232 | except Exception as e: 233 | LOG.exception(_("Can not add device %(device)s: %(msg)s"), 234 | {'device': device, 'msg': e}) 235 | continue 236 | 237 | def _treat_devices_removed(self, devices): 238 | for device in devices: 239 | LOG.info(_("Removing port %s"), device) 240 | try: 241 | if not device in self._port_map: 242 | LOG.warn(_("Can't find port %s in zvm agent"), device) 243 | continue 244 | 245 | self.port_unbound(device) 246 | self.plugin_rpc.update_device_down(self.context, 247 | device, 248 | self.agent_id) 249 | del self._port_map[device] 250 | except Exception as e: 251 | LOG.exception(_("Removing port failed %(device)s: %(msg)s"), 252 | {'device': device, 'msg': e}) 253 | continue 254 | 255 | def _process_network_ports(self, port_info): 256 | if len(port_info['added']): 257 | self._treat_devices_added(port_info['added']) 258 | if len(port_info['removed']): 259 | self._treat_devices_removed(port_info['removed']) 260 | 261 | def xcatdb_daemon_loop(self): 262 | ports = set() 263 | # Get all exsited ports as configured 264 | all_ports_info = self._update_ports(ports) 265 | if all_ports_info is not None: 266 | ports = all_ports_info['current'] 267 | 268 | connect = True 269 | while True: 270 | try: 271 | start_time = time.time() 272 | port_info = self._update_ports(ports) 273 | 274 | # if no exception is raised in _update_ports, 275 | # then the connection has recovered 276 | if connect is False: 277 | self._restart_handler.send(None) 278 | connect = True 279 | 280 | if port_info: 281 | LOG.debug(_("Devices change!")) 282 | self._process_network_ports(port_info) 283 | ports = port_info['current'] 284 | except exception.zVMxCatRequestFailed as e: 285 | LOG.error(_("Lost connection to xCAT. %s"), e) 286 | connect = False 287 | except Exception as e: 288 | LOG.exception(_("error in xCAT DB query loop: %s"), e) 289 | 290 | # sleep till end of polling interval 291 | elapsed = (time.time() - start_time) 292 | if (elapsed < self._polling_interval): 293 | sleep_time = self._polling_interval - elapsed 294 | LOG.debug(_("Sleep %s"), sleep_time) 295 | time.sleep(sleep_time) 296 | else: 297 | LOG.debug(_("Looping iteration exceeded interval")) 298 | 299 | def _init_xcat_mgt(self): 300 | '''xCAT Management Node(MN) use the first flat network to manage all 301 | the instances. So a flat network is required. 302 | To talk to xCAT MN, xCAT MN requires every instance has a NIC which is 303 | in the same subnet as xCAT. The xCAT MN's IP address is xcat_mgt_ip, 304 | mask is xcat_mgt_mask in the config file, 305 | by default neutron_zvm_plugin.ini. 306 | ''' 307 | 308 | if not len(cfg.CONF.ml2_type_flat.flat_networks): 309 | raise exception.zvmException( 310 | msg=_('Can not find xCAT management network,' 311 | 'a flat network is required by xCAT.')) 312 | self._utils.create_xcat_mgt_network(self._zhcp_node, 313 | cfg.CONF.AGENT.xcat_mgt_ip, 314 | cfg.CONF.AGENT.xcat_mgt_mask, 315 | cfg.CONF.ml2_type_flat.flat_networks[0]) 316 | 317 | @restart_wrapper 318 | def _handle_restart(self): 319 | xcat_uptime, zvm_uptime = (None, None) 320 | while True: 321 | LOG.info(_("Try to reinitialize network ... ")) 322 | try: 323 | tmp_new_time = self._utils.query_xcat_uptime(self._zhcp_node) 324 | if xcat_uptime != tmp_new_time: 325 | self._init_xcat_mgt() 326 | xcat_uptime = tmp_new_time 327 | 328 | tmp_new_time = self._utils.query_zvm_uptime(self._zhcp_node) 329 | if zvm_uptime != tmp_new_time: 330 | self._port_map = self._utils.re_grant_user(self._zhcp_node) 331 | zvm_uptime = tmp_new_time 332 | yield 333 | except Exception: 334 | LOG.error(_("Failed to handle restart," 335 | "try again in 5 seconds")) 336 | time.sleep(5) 337 | 338 | 339 | def main(): 340 | eventlet.monkey_patch() 341 | cfg.CONF(project='neutron') 342 | common_config.init(sys.argv[1:]) 343 | common_config.setup_logging() 344 | 345 | agent = zvmNeutronAgent() 346 | 347 | # Start to query xCAT DB 348 | agent.xcatdb_daemon_loop() 349 | LOG.info(_("z/VM agent initialized, now running... ")) 350 | sys.exit(0) 351 | -------------------------------------------------------------------------------- /neutron-zvm-plugin/neutron/plugins/zvm/common/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stackforge-attic/zvm-driver/cf77385929302987b63723484d3f78968f404e0d/neutron-zvm-plugin/neutron/plugins/zvm/common/__init__.py -------------------------------------------------------------------------------- /neutron-zvm-plugin/neutron/plugins/zvm/common/config.py: -------------------------------------------------------------------------------- 1 | # vim: tabstop=4 shiftwidth=4 softtabstop=4 2 | 3 | # Copyright 2014 IBM Corp. 4 | # 5 | # All Rights Reserved. 6 | # 7 | # Licensed under the Apache License, Version 2.0 (the "License"); you may 8 | # not use this file except in compliance with the License. You may obtain 9 | # a copy of the License at 10 | # 11 | # http://www.apache.org/licenses/LICENSE-2.0 12 | # 13 | # Unless required by applicable law or agreed to in writing, software 14 | # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 15 | # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 16 | # License for the specific language governing permissions and limitations 17 | # under the License. 18 | 19 | from oslo.config import cfg 20 | from neutron.agent.common import config 21 | from neutron.openstack.common.gettextutils import _ 22 | 23 | agent_opts = [ 24 | cfg.StrOpt( 25 | 'xcat_zhcp_nodename', 26 | default='zhcp', 27 | help=_('xCat zHCP nodename in xCAT ')), 28 | cfg.StrOpt( 29 | 'zvm_host', 30 | help=_('z/VM host that managed by zHCP.')), 31 | cfg.StrOpt( 32 | 'zvm_xcat_username', 33 | default='admin', 34 | help=_('xCat REST API username')), 35 | cfg.StrOpt( 36 | 'zvm_xcat_password', 37 | default='admin', 38 | secret=True, 39 | help=_('Password of the xCat REST API user')), 40 | cfg.StrOpt( 41 | 'zvm_xcat_server', 42 | help=_("xCat MN server address")), 43 | cfg.IntOpt( 44 | 'polling_interval', 45 | default=2, 46 | help=_("The number of seconds the agent will wait between " 47 | "polling for local device changes.")), 48 | cfg.IntOpt( 49 | 'zvm_xcat_timeout', 50 | default=300, 51 | help=_("The number of seconds the agent will wait for " 52 | "xCAT MN response")), 53 | cfg.StrOpt( 54 | 'xcat_mgt_ip', 55 | default=None, 56 | help=_("The IP address is used for xCAT MN to management instances.")), 57 | cfg.StrOpt( 58 | 'xcat_mgt_mask', 59 | default=None, 60 | help=_("The IP mask is used for xCAT MN to management instances.")), 61 | ] 62 | 63 | CONF = cfg.CONF 64 | CONF.register_opts(agent_opts, "AGENT") 65 | config.register_agent_state_opts_helper(cfg.CONF) 66 | config.register_root_helper(cfg.CONF) 67 | -------------------------------------------------------------------------------- /neutron-zvm-plugin/neutron/plugins/zvm/common/constants.py: -------------------------------------------------------------------------------- 1 | # vim: tabstop=4 shiftwidth=4 softtabstop=4 2 | 3 | # Copyright 2014 IBM Corp. 4 | # 5 | # Licensed under the Apache License, Version 2.0 (the "License"); you may 6 | # not use this file except in compliance with the License. You may obtain 7 | # a copy of the License at 8 | # 9 | # http://www.apache.org/licenses/LICENSE-2.0 10 | # 11 | # Unless required by applicable law or agreed to in writing, software 12 | # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 13 | # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 14 | # License for the specific language governing permissions and limitations 15 | # under the License. 16 | 17 | XCAT_RESPONSE_KEYS = ('info', 'data', 'node', 'errorcode', 'error') 18 | -------------------------------------------------------------------------------- /neutron-zvm-plugin/neutron/plugins/zvm/common/exception.py: -------------------------------------------------------------------------------- 1 | # vim: tabstop=4 shiftwidth=4 softtabstop=4 2 | 3 | # Copyright 2014 IBM Corp. 4 | # 5 | # Licensed under the Apache License, Version 2.0 (the "License"); you may 6 | # not use this file except in compliance with the License. You may obtain 7 | # a copy of the License at 8 | # 9 | # http://www.apache.org/licenses/LICENSE-2.0 10 | # 11 | # Unless required by applicable law or agreed to in writing, software 12 | # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 13 | # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 14 | # License for the specific language governing permissions and limitations 15 | # under the License. 16 | 17 | 18 | from neutron.common import exceptions as exception 19 | from neutron.openstack.common.gettextutils import _ 20 | 21 | 22 | class zvmException(exception.NeutronException): 23 | message = _('zvmException: %(msg)s') 24 | 25 | 26 | class zVMxCatConnectionFailed(exception.NeutronException): 27 | message = _('Failed to connect xCAT server: %(xcatserver)s') 28 | 29 | 30 | class zVMxCatRequestFailed(exception.NeutronException): 31 | message = _('Request to xCAT server %(xcatserver)s failed: %(err)s') 32 | 33 | 34 | class zVMJsonLoadsError(exception.NeutronException): 35 | message = _('JSON loads error: not in JSON format') 36 | 37 | 38 | class zVMInvalidDataError(exception.NeutronException): 39 | message = _('Invalid data error: %(msg)s') 40 | 41 | 42 | class zVMInvalidxCatResponseDataError(exception.NeutronException): 43 | message = _('Invalid data returned from xCAT: %(msg)s') 44 | -------------------------------------------------------------------------------- /neutron-zvm-plugin/neutron/plugins/zvm/common/utils.py: -------------------------------------------------------------------------------- 1 | # vim: tabstop=4 shiftwidth=4 softtabstop=4 2 | 3 | # Copyright 2014 IBM Corp. 4 | # 5 | # Licensed under the Apache License, Version 2.0 (the "License"); you may 6 | # not use this file except in compliance with the License. You may obtain 7 | # a copy of the License at 8 | # 9 | # http://www.apache.org/licenses/LICENSE-2.0 10 | # 11 | # Unless required by applicable law or agreed to in writing, software 12 | # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 13 | # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 14 | # License for the specific language governing permissions and limitations 15 | # under the License. 16 | 17 | import xcatutils 18 | 19 | from oslo.config import cfg 20 | from neutron.openstack.common import log as logging 21 | from neutron.openstack.common.gettextutils import _ 22 | from neutron.plugins.zvm.common import exception 23 | 24 | CONF = cfg.CONF 25 | LOG = logging.getLogger(__name__) 26 | 27 | 28 | class zvmUtils(object): 29 | _MAX_REGRANT_USER_NUMBER = 1000 30 | 31 | def __init__(self): 32 | self._xcat_url = xcatutils.xCatURL() 33 | self._zhcp_userid = None 34 | self._userid_map = {} 35 | self._xcat_node_name = self._get_xcat_node_name() 36 | 37 | def get_node_from_port(self, port_id): 38 | return self._get_nic_settings(port_id, get_node=True) 39 | 40 | def get_nic_ids(self): 41 | addp = '' 42 | url = self._xcat_url.tabdump("/switch", addp) 43 | nic_settings = xcatutils.xcat_request("GET", url) 44 | # remove table header 45 | nic_settings['data'][0].pop(0) 46 | # it's possible to return empty array 47 | return nic_settings['data'][0] 48 | 49 | def _get_nic_settings(self, port_id, field=None, get_node=False): 50 | """Get NIC information from xCat switch table.""" 51 | LOG.debug(_("Get nic information for port: %s"), port_id) 52 | addp = '&col=port&value=%s' % port_id + '&attribute=%s' % ( 53 | field and field or 'node') 54 | url = self._xcat_url.gettab("/switch", addp) 55 | nic_settings = xcatutils.xcat_request("GET", url) 56 | ret_value = nic_settings['data'][0][0] 57 | if field is None and not get_node: 58 | ret_value = self.get_userid_from_node(ret_value) 59 | return ret_value 60 | 61 | def get_userid_from_node(self, node): 62 | addp = '&col=node&value=%s&attribute=userid' % node 63 | url = self._xcat_url.gettab("/zvm", addp) 64 | user_info = xcatutils.xcat_request("GET", url) 65 | return user_info['data'][0][0] 66 | 67 | def couple_nic_to_vswitch(self, vswitch_name, switch_port_name, 68 | zhcp, userid, dm=True, immdt=True): 69 | """Couple nic to vswitch.""" 70 | LOG.debug(_("Connect nic to switch: %s"), vswitch_name) 71 | vdev = self._get_nic_settings(switch_port_name, "interface") 72 | if vdev: 73 | self._couple_nic(zhcp, vswitch_name, userid, vdev, dm, immdt) 74 | else: 75 | raise exception.zVMInvalidDataError(msg=('Cannot get vdev for ' 76 | 'user %s, couple to port %s') % 77 | (userid, switch_port_name)) 78 | return vdev 79 | 80 | def uncouple_nic_from_vswitch(self, vswitch_name, switch_port_name, 81 | zhcp, userid, dm=True, immdt=True): 82 | """Uncouple nic from vswitch.""" 83 | LOG.debug(_("Disconnect nic from switch: %s"), vswitch_name) 84 | vdev = self._get_nic_settings(switch_port_name, "interface") 85 | self._uncouple_nic(zhcp, userid, vdev, dm, immdt) 86 | 87 | def set_vswitch_port_vlan_id(self, vlan_id, switch_port_name, vdev, zhcp, 88 | vswitch_name): 89 | userid = self._get_nic_settings(switch_port_name) 90 | if not userid: 91 | raise exception.zVMInvalidDataError(msg=('Cannot get userid by ' 92 | 'port %s') % (switch_port_name)) 93 | url = self._xcat_url.xdsh("/%s" % zhcp) 94 | commands = '/opt/zhcp/bin/smcli Virtual_Network_Vswitch_Set_Extended' 95 | commands += " -T %s" % userid 96 | commands += ' -k grant_userid=%s' % userid 97 | commands += " -k switch_name=%s" % vswitch_name 98 | commands += " -k user_vlan_id=%s" % vlan_id 99 | xdsh_commands = 'command=%s' % commands 100 | body = [xdsh_commands] 101 | xcatutils.xcat_request("PUT", url, body) 102 | 103 | def grant_user(self, zhcp, vswitch_name, userid): 104 | """Set vswitch to grant user.""" 105 | url = self._xcat_url.xdsh("/%s" % zhcp) 106 | commands = '/opt/zhcp/bin/smcli Virtual_Network_Vswitch_Set_Extended' 107 | commands += " -T %s" % userid 108 | commands += " -k switch_name=%s" % vswitch_name 109 | commands += " -k grant_userid=%s" % userid 110 | xdsh_commands = 'command=%s' % commands 111 | body = [xdsh_commands] 112 | xcatutils.xcat_request("PUT", url, body) 113 | 114 | def revoke_user(self, zhcp, vswitch_name, userid): 115 | """Set vswitch to grant user.""" 116 | url = self._xcat_url.xdsh("/%s" % zhcp) 117 | commands = '/opt/zhcp/bin/smcli Virtual_Network_Vswitch_Set_Extended' 118 | commands += " -T %s" % userid 119 | commands += " -k switch_name=%s" % vswitch_name 120 | commands += " -k revoke_userid=%s" % userid 121 | xdsh_commands = 'command=%s' % commands 122 | body = [xdsh_commands] 123 | xcatutils.xcat_request("PUT", url, body) 124 | 125 | def _couple_nic(self, zhcp, vswitch_name, userid, vdev, dm, immdt): 126 | """Couple NIC to vswitch by adding vswitch into user direct.""" 127 | url = self._xcat_url.xdsh("/%s" % zhcp) 128 | if dm: 129 | commands = '/opt/zhcp/bin/smcli' 130 | commands += ' Virtual_Network_Adapter_Connect_Vswitch_DM' 131 | commands += " -T %s " % userid + "-v %s" % vdev 132 | commands += " -n %s" % vswitch_name 133 | xdsh_commands = 'command=%s' % commands 134 | body = [xdsh_commands] 135 | xcatutils.xcat_request("PUT", url, body) 136 | if immdt: 137 | # the inst must be active, or this call will failed 138 | commands = '/opt/zhcp/bin/smcli' 139 | commands += ' Virtual_Network_Adapter_Connect_Vswitch' 140 | commands += " -T %s " % userid + "-v %s" % vdev 141 | commands += " -n %s" % vswitch_name 142 | xdsh_commands = 'command=%s' % commands 143 | body = [xdsh_commands] 144 | xcatutils.xcat_request("PUT", url, body) 145 | 146 | def _uncouple_nic(self, zhcp, userid, vdev, dm, immdt): 147 | """Couple NIC to vswitch by adding vswitch into user direct.""" 148 | url = self._xcat_url.xdsh("/%s" % zhcp) 149 | if dm: 150 | commands = '/opt/zhcp/bin/smcli' 151 | commands += ' Virtual_Network_Adapter_Disconnect_DM' 152 | commands += " -T %s " % userid + "-v %s" % vdev 153 | xdsh_commands = 'command=%s' % commands 154 | body = [xdsh_commands] 155 | xcatutils.xcat_request("PUT", url, body) 156 | if immdt: 157 | # the inst must be active, or this call will failed 158 | commands = '/opt/zhcp/bin/smcli' 159 | commands += ' Virtual_Network_Adapter_Disconnect' 160 | commands += " -T %s " % userid + "-v %s" % vdev 161 | xdsh_commands = 'command=%s' % commands 162 | body = [xdsh_commands] 163 | xcatutils.xcat_request("PUT", url, body) 164 | 165 | def put_user_direct_online(self, zhcp, userid): 166 | url = self._xcat_url.xdsh("/%s" % zhcp) 167 | commands = '/opt/zhcp/bin/smcli Static_Image_Changes_Immediate_DM' 168 | commands += " -T %s" % userid 169 | xdsh_commands = 'command=%s' % commands 170 | body = [xdsh_commands] 171 | xcatutils.xcat_request("PUT", url, body) 172 | 173 | def get_zhcp_userid(self, zhcp): 174 | if not self._zhcp_userid: 175 | self._zhcp_userid = self.get_userid_from_node(zhcp) 176 | return self._zhcp_userid 177 | 178 | def add_vswitch(self, zhcp, name, rdev, 179 | controller='*', 180 | connection=1, queue_mem=8, router=0, network_type=2, vid=0, 181 | port_type=1, update=1, gvrp=2, native_vid=1): 182 | ''' 183 | connection:0-unspecified 1-Actice 2-non-Active 184 | router:0-unspecified 1-nonrouter 2-prirouter 185 | type:0-unspecified 1-IP 2-ethernet 186 | vid:1-4094 for access port defaut vlan 187 | port_type:0-unspecified 1-access 2-trunk 188 | update:0-unspecified 1-create 2-create and add to system 189 | configuration file 190 | gvrp:0-unspecified 1-gvrp 2-nogvrp 191 | ''' 192 | if (self._does_vswitch_exist(zhcp, name)): 193 | LOG.info(_('Vswitch %s already exists.'), name) 194 | return 195 | 196 | # if vid = 0, port_type, gvrp and native_vlanid are not 197 | # allowed to specified 198 | if not len(vid): 199 | vid = 0 200 | port_type = 0 201 | gvrp = 0 202 | native_vid = -1 203 | else: 204 | vid = str(vid[0][0]) + '-' + str(vid[0][1]) 205 | 206 | userid = self.get_zhcp_userid(zhcp) 207 | url = self._xcat_url.xdsh("/%s" % zhcp) 208 | commands = '/opt/zhcp/bin/smcli Virtual_Network_Vswitch_Create' 209 | commands += " -T %s" % userid 210 | commands += ' -n %s' % name 211 | if rdev: 212 | commands += " -r %s" % rdev.replace(',', ' ') 213 | #commands += " -a %s" % osa_name 214 | if controller != '*': 215 | commands += " -i %s" % controller 216 | commands += " -c %s" % connection 217 | commands += " -q %s" % queue_mem 218 | commands += " -e %s" % router 219 | commands += " -t %s" % network_type 220 | commands += " -v %s" % vid 221 | commands += " -p %s" % port_type 222 | commands += " -u %s" % update 223 | commands += " -G %s" % gvrp 224 | commands += " -V %s" % native_vid 225 | xdsh_commands = 'command=%s' % commands 226 | body = [xdsh_commands] 227 | 228 | result = xcatutils.xcat_request("PUT", url, body) 229 | if (result['errorcode'][0][0] != '0') or \ 230 | (not self._does_vswitch_exist(zhcp, name)): 231 | raise exception.zvmException( 232 | msg=("switch: %s add failed, %s") % 233 | (name, result['data'][0][0])) 234 | LOG.info(_('Created vswitch %s done.'), name) 235 | 236 | def _does_vswitch_exist(self, zhcp, vsw): 237 | userid = self.get_zhcp_userid(zhcp) 238 | url = self._xcat_url.xdsh("/%s" % zhcp) 239 | commands = '/opt/zhcp/bin/smcli Virtual_Network_Vswitch_Query' 240 | commands += " -T %s" % userid 241 | commands += " -s %s" % vsw 242 | xdsh_commands = 'command=%s' % commands 243 | body = [xdsh_commands] 244 | result = xcatutils.xcat_request("PUT", url, body) 245 | 246 | return (result['errorcode'][0][0] == '0') 247 | 248 | def re_grant_user(self, zhcp): 249 | """Grant user again after z/VM is re-IPLed""" 250 | ports_info = self._get_userid_vswitch_vlan_id_mapping(zhcp) 251 | records_num = 0 252 | cmd = '' 253 | 254 | def run_command(command): 255 | xdsh_commands = 'command=%s' % command 256 | body = [xdsh_commands] 257 | url = self._xcat_url.xdsh("/%s" % zhcp) 258 | xcatutils.xcat_request("PUT", url, body) 259 | 260 | for (port_id, port) in ports_info.items(): 261 | if port['userid'] is None or port['vswitch'] is None: 262 | continue 263 | if len(port['userid']) == 0 or len(port['vswitch']) == 0: 264 | continue 265 | 266 | cmd += '/opt/zhcp/bin/smcli ' 267 | cmd += 'Virtual_Network_Vswitch_Set_Extended ' 268 | cmd += '-T %s ' % port['userid'] 269 | cmd += '-k switch_name=%s ' % port['vswitch'] 270 | cmd += '-k grant_userid=%s' % port['userid'] 271 | try: 272 | if int(port['vlan_id']) in range(1, 4094): 273 | cmd += ' -k user_vlan_id=%s\n' % port['vlan_id'] 274 | else: 275 | cmd += '\n' 276 | except ValueError: 277 | # just in case there are bad records of vlan info which 278 | # could be a string 279 | LOG.warn(_("Unknown vlan '%(vlan)s' for user %(user)s."), 280 | {'vlan': port['vlan_id'], 'user': port['userid']}) 281 | cmd += '\n' 282 | continue 283 | records_num += 1 284 | if records_num >= self._MAX_REGRANT_USER_NUMBER: 285 | try: 286 | commands = 'echo -e "#!/bin/sh\n%s" > grant.sh' % cmd[:-1] 287 | run_command(commands) 288 | commands = 'sh grant.sh;rm -f grant.sh' 289 | run_command(commands) 290 | records_num = 0 291 | cmd = '' 292 | except Exception: 293 | LOG.warn(_("Grant user failed")) 294 | 295 | if len(cmd) > 0: 296 | commands = 'echo -e "#!/bin/sh\n%s" > grant.sh' % cmd[:-1] 297 | run_command(commands) 298 | commands = 'sh grant.sh;rm -f grant.sh' 299 | run_command(commands) 300 | return ports_info 301 | 302 | def _get_userid_vswitch_vlan_id_mapping(self, zhcp): 303 | ports_info = self.get_nic_ids() 304 | ports = {} 305 | for p in ports_info: 306 | port_info = p.split(',') 307 | target_host = port_info[5].strip('"') 308 | port_vid = port_info[3].strip('"') 309 | port_id = port_info[2].strip('"') 310 | vswitch = port_info[1].strip('"') 311 | nodename = port_info[0].strip('"') 312 | if target_host == zhcp: 313 | ports[port_id] = {'nodename': nodename, 314 | 'vswitch': vswitch, 315 | 'userid': None, 316 | 'vlan_id': port_vid} 317 | 318 | def get_all_userid(): 319 | users = {} 320 | addp = '' 321 | url = self._xcat_url.tabdump("/zvm", addp) 322 | all_userids = xcatutils.xcat_request("GET", url) 323 | header = '#node,hcp,userid,nodetype,parent,comments,disable' 324 | all_userids['data'][0].remove(header) 325 | if len(all_userids) > 0: 326 | for u in all_userids['data'][0]: 327 | user_info = u.split(',') 328 | userid = user_info[2].strip('"') 329 | nodename = user_info[0].strip('"') 330 | users[nodename] = {'userid': userid} 331 | 332 | return users 333 | 334 | users = get_all_userid() 335 | 336 | for (port_id, port) in ports.items(): 337 | try: 338 | ports[port_id]['userid'] = users[port['nodename']]['userid'] 339 | except Exception: 340 | LOG.info(_("Garbage port found. port id: %s") % port_id) 341 | 342 | return ports 343 | 344 | def update_xcat_switch(self, port, vswitch, vlan): 345 | """Update information in xCAT switch table.""" 346 | commands = "port=%s" % port 347 | commands += " switch.switch=%s" % vswitch 348 | commands += " switch.vlan=%s" % (vlan and vlan or -1) 349 | url = self._xcat_url.tabch("/switch") 350 | body = [commands] 351 | xcatutils.xcat_request("PUT", url, body) 352 | 353 | def create_xcat_mgt_network(self, zhcp, mgt_ip, mgt_mask, mgt_vswitch): 354 | url = self._xcat_url.xdsh("/%s" % zhcp) 355 | xdsh_commands = ('command=smcli Virtual_Network_Adapter_Query' 356 | ' -T %s -v 0800') % self._xcat_node_name 357 | body = [xdsh_commands] 358 | result = xcatutils.xcat_request("PUT", url, body)['data'][0][0] 359 | code = result.split("\n") 360 | # return code 212: Adapter does not exist 361 | new_nic = '' 362 | if len(code) == 4 and code[1].split(': ')[2] == '212': 363 | new_nic = ('vmcp define nic 0800 type qdio\n' + 364 | 'vmcp couple 0800 system %s\n' % (mgt_vswitch)) 365 | elif len(code) == 7: 366 | status = code[4].split(': ')[2] 367 | if status == 'Coupled and active': 368 | # we just assign the IP/mask, 369 | # no matter if it is assigned or not 370 | LOG.info(_("Assign IP for NIC 800.")) 371 | else: 372 | LOG.error(_("NIC 800 staus is unknown.")) 373 | return 374 | else: 375 | raise exception.zvmException( 376 | msg="Unknown information from SMAPI") 377 | 378 | url = self._xcat_url.xdsh("/%s") % self._xcat_node_name 379 | cmd = new_nic + ('/usr/bin/perl /usr/sbin/sspqeth2.pl ' + 380 | '-a %s -d 0800 0801 0802 -e eth2 -m %s -g %s' 381 | % (mgt_ip, mgt_mask, mgt_ip)) 382 | xdsh_commands = 'command=%s' % cmd 383 | body = [xdsh_commands] 384 | xcatutils.xcat_request("PUT", url, body) 385 | 386 | def _get_xcat_node_ip(self): 387 | addp = '&col=key&value=master&attribute=value' 388 | url = self._xcat_url.gettab("/site", addp) 389 | return xcatutils.xcat_request("GET", url)['data'][0][0] 390 | 391 | def _get_xcat_node_name(self): 392 | xcat_ip = self._get_xcat_node_ip() 393 | addp = '&col=ip&value=%s&attribute=node' % (xcat_ip) 394 | url = self._xcat_url.gettab("/hosts", addp) 395 | return (xcatutils.xcat_request("GET", url)['data'][0][0]) 396 | 397 | def query_xcat_uptime(self, zhcp): 398 | url = self._xcat_url.xdsh("/%s" % zhcp) 399 | cmd = '/opt/zhcp/bin/smcli Image_Query_Activate_Time' 400 | cmd += " -T %s" % self.get_userid_from_node( 401 | self._xcat_node_name) 402 | # format 4: yyyy-mm-dd 403 | cmd += " -f %s" % "4" 404 | xdsh_commands = 'command=%s' % cmd 405 | body = [xdsh_commands] 406 | ret_str = xcatutils.xcat_request("PUT", url, body)['data'][0][0] 407 | return ret_str.split('on ')[1] 408 | 409 | def query_zvm_uptime(self, zhcp): 410 | url = self._xcat_url.xdsh("/%s" % zhcp) 411 | cmd = '/opt/zhcp/bin/smcli System_Info_Query' 412 | xdsh_commands = 'command=%s' % cmd 413 | body = [xdsh_commands] 414 | ret_str = xcatutils.xcat_request("PUT", url, body)['data'][0][0] 415 | return ret_str.split('\n')[4].split(': ', 3)[2] 416 | -------------------------------------------------------------------------------- /neutron-zvm-plugin/neutron/plugins/zvm/common/xcatutils.py: -------------------------------------------------------------------------------- 1 | # vim: tabstop=4 shiftwidth=4 softtabstop=4 2 | 3 | # Copyright 2014 IBM Corp. 4 | # 5 | # Licensed under the Apache License, Version 2.0 (the "License"); you may 6 | # not use this file except in compliance with the License. You may obtain 7 | # a copy of the License at 8 | # 9 | # http://www.apache.org/licenses/LICENSE-2.0 10 | # 11 | # Unless required by applicable law or agreed to in writing, software 12 | # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 13 | # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 14 | # License for the specific language governing permissions and limitations 15 | # under the License. 16 | 17 | 18 | import functools 19 | import httplib 20 | 21 | from neutron.openstack.common import jsonutils 22 | from neutron.openstack.common.gettextutils import _ 23 | from neutron.openstack.common import log as logging 24 | from neutron.plugins.zvm.common import config 25 | from neutron.plugins.zvm.common import constants 26 | from neutron.plugins.zvm.common import exception 27 | 28 | LOG = logging.getLogger(__name__) 29 | CONF = config.CONF 30 | 31 | 32 | class xCatURL(object): 33 | """To return xCat url for invoking xCat REST API.""" 34 | 35 | def __init__(self): 36 | """Set constant that used to form xCat url.""" 37 | self.PREFIX = '/xcatws' 38 | self.SUFFIX = '?userName=' + CONF.AGENT.zvm_xcat_username + \ 39 | '&password=' + CONF.AGENT.zvm_xcat_password + \ 40 | '&format=json' 41 | 42 | self.NODES = '/nodes' 43 | self.TABLES = '/tables' 44 | self.XDSH = '/dsh' 45 | 46 | def tabdump(self, arg='', addp=None): 47 | rurl = self.PREFIX + self.TABLES + arg + self.SUFFIX 48 | return self._append_addp(rurl, addp) 49 | 50 | def _append_addp(self, rurl, addp=None): 51 | if addp is not None: 52 | return rurl + addp 53 | else: 54 | return rurl 55 | 56 | def gettab(self, arg='', addp=None): 57 | """Get table arg, with attribute addp.""" 58 | rurl = self.PREFIX + self.TABLES + arg + self.SUFFIX 59 | return self._append_addp(rurl, addp) 60 | 61 | def tabch(self, arg='', addp=None): 62 | """Add/update/delete row(s) in table arg, with attribute addp.""" 63 | rurl = self.PREFIX + self.TABLES + arg + self.SUFFIX 64 | return self._append_addp(rurl, addp) 65 | 66 | def xdsh(self, arg=''): 67 | """Run shell command.""" 68 | return self.PREFIX + self.NODES + arg + self.XDSH + self.SUFFIX 69 | 70 | 71 | class xCatConnection(): 72 | """Https requests to xCat web service.""" 73 | def __init__(self): 74 | """Initialize https connection to xCat service.""" 75 | self.host = CONF.AGENT.zvm_xcat_server 76 | self.xcat_timeout = CONF.AGENT.zvm_xcat_timeout 77 | try: 78 | self.conn = httplib.HTTPSConnection(self.host, None, None, None, 79 | True, self.xcat_timeout) 80 | except Exception: 81 | LOG.error(_("Connect to xCat server %s failed") % self.host) 82 | raise exception.zVMxCatConnectionFailed(xcatserver=self.host) 83 | 84 | def request(self, method, url, body=None, headers={}): 85 | """Do http request to xCat server 86 | 87 | Will return (response_status, response_reason, response_body) 88 | """ 89 | if body is not None: 90 | body = jsonutils.dumps(body) 91 | headers = {'content-type': 'text/plain', 92 | 'content-length': len(body)} 93 | 94 | try: 95 | self.conn.request(method, url, body, headers) 96 | except Exception as err: 97 | LOG.error(_("Request to xCat server %(host)s failed: %(err)s") % 98 | {'host': self.host, 'err': err}) 99 | raise exception.zVMxCatRequestFailed(xcatserver=self.host, 100 | err=err) 101 | 102 | res = self.conn.getresponse() 103 | msg = res.read() 104 | resp = { 105 | 'status': res.status, 106 | 'reason': res.reason, 107 | 'message': msg} 108 | 109 | # NOTE(rui): Currently, only xCat returns 200 or 201 can be 110 | # considered acceptable. 111 | err = None 112 | if method == "POST": 113 | if res.status != 201: 114 | err = str(resp) 115 | else: 116 | if res.status != 200: 117 | err = str(resp) 118 | 119 | if err is not None: 120 | LOG.error(_("Request to xCat server %(host)s failed: %(err)s") % 121 | {'host': self.host, 'err': err}) 122 | raise exception.zVMxCatRequestFailed(xcatserver=self.host, 123 | err=err) 124 | 125 | return resp 126 | 127 | 128 | def xcat_request(method, url, body=None, headers={}): 129 | conn = xCatConnection() 130 | resp = conn.request(method, url, body, headers) 131 | return load_xcat_resp(resp['message']) 132 | 133 | 134 | def jsonloads(jsonstr): 135 | try: 136 | return jsonutils.loads(jsonstr) 137 | except ValueError: 138 | LOG.error(_("Respone is not in JSON format")) 139 | raise exception.zVMJsonLoadsError() 140 | 141 | 142 | def wrap_invalid_xcat_resp_data_error(function): 143 | """zVM driver get zVM hypervisor and virtual machine information 144 | from xCat. xCat REST API response has its own fixed format(a JSON 145 | stream). zVM driver abstract useful info base on the special format, 146 | and raise exception if the data in incorrect format. 147 | """ 148 | @functools.wraps(function) 149 | def decorated_function(*arg, **kwargs): 150 | try: 151 | return function(*arg, **kwargs) 152 | except (ValueError, TypeError, IndexError) as err: 153 | LOG.error(_('Invalid data returned from xCat: %s') % err) 154 | raise exception.zVMInvalidxCatResponseDataError(msg=err) 155 | except Exception as err: 156 | raise 157 | 158 | return decorated_function 159 | 160 | 161 | @wrap_invalid_xcat_resp_data_error 162 | def load_xcat_resp(message): 163 | """Abstract information from xCat REST response body. 164 | 165 | As default, xCat response will in format of JSON and can be 166 | converted to Python dictionary, would looks like: 167 | {"data": [{"info": [info,]}, {"data": [data,]}, ..., {"error": [error,]}]} 168 | 169 | Returns a Python dictionary, looks like: 170 | {'info': [info,], 171 | 'data': [data,], 172 | 'error': [error,]} 173 | """ 174 | resp_list = jsonloads(message)['data'] 175 | keys = constants.XCAT_RESPONSE_KEYS 176 | 177 | resp = {} 178 | try: 179 | for k in keys: 180 | resp[k] = [] 181 | 182 | for d in resp_list: 183 | for k in keys: 184 | if d.get(k) is not None: 185 | resp[k].append(d.get(k)) 186 | except Exception: 187 | LOG.error(_("Invalid data returned from xCat: %s") % message) 188 | raise exception.zVMInvalidxCatResponseDataError(msg=message) 189 | 190 | if not verify_xcat_resp(resp): 191 | LOG.error(_("Error returned from xCAT: %s") % message) 192 | raise exception.zVMInvalidxCatResponseDataError(msg=message) 193 | else: 194 | return resp 195 | 196 | 197 | @wrap_invalid_xcat_resp_data_error 198 | def verify_xcat_resp(resp_dict): 199 | """Check whether xCAT REST API response contains an error.""" 200 | if resp_dict.get('error'): 201 | if resp_dict['error'][0][0].find('Warning'): 202 | return True 203 | return False 204 | else: 205 | return True 206 | -------------------------------------------------------------------------------- /neutron-zvm-plugin/neutron/tests/unit/zvm/test_zvm_network.py: -------------------------------------------------------------------------------- 1 | # vim: tabstop=4 shiftwidth=4 softtabstop=4 2 | 3 | # Copyright 2014 IBM Corp. 4 | # 5 | # Licensed under the Apache License, Version 2.0 (the "License"); you may 6 | # not use this file except in compliance with the License. You may obtain 7 | # a copy of the License at 8 | # 9 | # http://www.apache.org/licenses/LICENSE-2.0 10 | # 11 | # Unless required by applicable law or agreed to in writing, software 12 | # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 13 | # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 14 | # License for the specific language governing permissions and limitations 15 | # under the License. 16 | 17 | """ 18 | Unit tests for the z/VM network. 19 | """ 20 | 21 | import mock 22 | 23 | from oslo.config import cfg 24 | from neutron.plugins.zvm.agent import zvm_network 25 | from neutron.tests import base 26 | 27 | FLAT_NETWORKS = ['flat_net1'] 28 | VLAN_NETWORKS = ['vlan_net1:100:500'] 29 | NETWORK_MAPS = {'vlan_net1': [(100, 500)], 'flat_net1': []} 30 | 31 | 32 | class TestZVMNetwork(base.BaseTestCase): 33 | 34 | _FAKE_NETWORK_VLAN_RANGES = "fakevsw1:1:4094,fakevsw2,fakevsw3:2:2999" 35 | 36 | def setUp(self): 37 | super(TestZVMNetwork, self).setUp() 38 | cfg.CONF.set_override('flat_networks', FLAT_NETWORKS, 39 | group='ml2_type_flat') 40 | cfg.CONF.set_override('network_vlan_ranges', VLAN_NETWORKS, 41 | group='ml2_type_vlan') 42 | 43 | with mock.patch('neutron.plugins.zvm.common.utils.zvmUtils') as utils: 44 | self._zvm_network = zvm_network.zvmNetwork() 45 | 46 | def test_get_network_maps(self): 47 | maps = self._zvm_network.get_network_maps() 48 | self.assertEqual(maps, NETWORK_MAPS) 49 | -------------------------------------------------------------------------------- /neutron-zvm-plugin/neutron/tests/unit/zvm/test_zvm_neutron_agent.py: -------------------------------------------------------------------------------- 1 | # vim: tabstop=4 shiftwidth=4 softtabstop=4 2 | 3 | # Copyright 2014 IBM Corp. 4 | # 5 | # Licensed under the Apache License, Version 2.0 (the "License"); you may 6 | # not use this file except in compliance with the License. You may obtain 7 | # a copy of the License at 8 | # 9 | # http://www.apache.org/licenses/LICENSE-2.0 10 | # 11 | # Unless required by applicable law or agreed to in writing, software 12 | # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 13 | # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 14 | # License for the specific language governing permissions and limitations 15 | # under the License. 16 | 17 | """ 18 | Unit tests for neutron z/VM driver 19 | """ 20 | 21 | import mock 22 | 23 | from neutron.plugins.zvm.agent import zvm_neutron_agent 24 | from neutron.tests import base 25 | from oslo.config import cfg 26 | 27 | FLAT_NETWORKS = ['flat_net1'] 28 | VLAN_NETWORKS = ['vlan_net1:100:500'] 29 | NET_UUID = 'zvm-net-uuid' 30 | PORT_UUID = 'zvm-port-uuid' 31 | 32 | 33 | class FakeLoopingCall(object): 34 | def __init__(self, fake_time): 35 | self.fake_time = fake_time 36 | 37 | def start(self, interval=0): 38 | self.fake_time() 39 | 40 | 41 | class TestZVMNeutronAgent(base.BaseTestCase): 42 | 43 | def setUp(self): 44 | super(TestZVMNeutronAgent, self).setUp() 45 | self.addCleanup(cfg.CONF.reset) 46 | cfg.CONF.set_override('rpc_backend', 47 | 'neutron.openstack.common.rpc.impl_fake') 48 | cfg.CONF.set_override('flat_networks', FLAT_NETWORKS, 49 | group='ml2_type_flat') 50 | cfg.CONF.set_override('network_vlan_ranges', VLAN_NETWORKS, 51 | group='ml2_type_vlan') 52 | 53 | mock.patch('neutron.openstack.common.loopingcall.' 54 | 'FixedIntervalLoopingCall', 55 | new=FakeLoopingCall) 56 | with mock.patch( 57 | 'neutron.plugins.zvm.common.utils.zvmUtils') as mock_Utils: 58 | instance = mock_Utils.return_value 59 | get_zhcp_userid = mock.MagicMock(return_value='zhcp_user') 60 | create_xcat_mgt_network = mock.MagicMock() 61 | 62 | instance.get_zhcp_userid = get_zhcp_userid 63 | instance.create_xcat_mgt_network = create_xcat_mgt_network 64 | net_attrs = {'fake_uuid1': { 65 | 'vswitch': 'fake_vsw', 'userid': 'fake_user1'}} 66 | instance.re_grant_user = mock.MagicMock(return_value=net_attrs) 67 | instance.query_xcat_uptime = mock.MagicMock( 68 | return_value="xcat uptime 1") 69 | instance.query_zvm_uptime = mock.MagicMock( 70 | return_value="zvm uptime 1") 71 | 72 | self.agent = zvm_neutron_agent.zvmNeutronAgent() 73 | self.agent.plugin_rpc = mock.Mock() 74 | self.agent.context = mock.Mock() 75 | self.agent.agent_id = mock.Mock() 76 | 77 | def test_port_bound_vlan(self): 78 | vid = 100 79 | with mock.patch.object(zvm_neutron_agent, "LOG") as log: 80 | self._test_port_bound('vlan', vid) 81 | log.info.assert_called_with('Binding VLAN, VLAN ID: %s', vid) 82 | 83 | def test_port_bound_flat(self): 84 | with mock.patch.object(zvm_neutron_agent, "LOG") as log: 85 | self._test_port_bound('flat') 86 | log.info.assert_called_with('Bind %s mode done', 'flat') 87 | 88 | def _test_port_bound(self, network_type, vid=None): 89 | port = mock.MagicMock() 90 | net_uuid = NET_UUID 91 | mock_enable_vlan = mock.MagicMock() 92 | enable_vlan = False 93 | 94 | if network_type == 'vlan': 95 | enable_vlan = True 96 | 97 | with mock.patch.multiple( 98 | self.agent._utils, 99 | couple_nic_to_vswitch=mock.MagicMock(), 100 | put_user_direct_online=mock.MagicMock(), 101 | set_vswitch_port_vlan_id=mock_enable_vlan): 102 | 103 | self.agent.port_bound(port, net_uuid, network_type, None, 104 | vid, 'fake_user') 105 | 106 | self.assertEqual(enable_vlan, mock_enable_vlan.called) 107 | 108 | def test_port_unbound(self): 109 | # port_unbound just call utils.revoke_user, revoke_user is covered 110 | # in test_zvm_utils 111 | pass 112 | 113 | def test_treat_devices_added_returns_true_for_missing_device(self): 114 | attrs = {'get_device_details.side_effect': Exception()} 115 | self.agent.plugin_rpc.configure_mock(**attrs) 116 | # no exception should be raised 117 | self.agent._treat_devices_added([]) 118 | 119 | def test_treat_devices_added_down_port(self): 120 | details = dict(port_id='added_port_down', physical_network='vsw', 121 | segmentation_id='10', network_id='fake_net', 122 | network_type='flat', admin_state_up=False) 123 | attrs = {'get_device_details.return_value': details} 124 | self.agent.plugin_rpc.configure_mock(**attrs) 125 | with mock.patch.object(self.agent, "_treat_vif_port", 126 | mock.Mock(return_value=('fake_node', 'fake_user'))): 127 | self.agent._treat_devices_added(['added_port_down']) 128 | self.assertTrue(self.agent.plugin_rpc.update_device_down.called) 129 | 130 | def test_treat_devices_added_up_port(self): 131 | details = dict(port_id='added_port', physical_network='vsw', 132 | segmentation_id='10', network_id='fake_net', 133 | network_type='flat', admin_state_up=True) 134 | attrs = {'get_device_details.return_value': details} 135 | self.agent.plugin_rpc.configure_mock(**attrs) 136 | with mock.patch.object(self.agent, "_treat_vif_port", 137 | mock.Mock(return_value=('fake_node', 'fake_user'))): 138 | self.agent._treat_devices_added(['added_port']) 139 | self.assertTrue(self.agent.plugin_rpc.get_device_details.called) 140 | 141 | def test_treat_devices_added_missing_port_id(self): 142 | details = mock.MagicMock() 143 | details.__contains__.side_effect = lambda x: False 144 | attrs = {'get_device_details.return_value': details} 145 | self.agent.plugin_rpc.configure_mock(**attrs) 146 | with mock.patch.object(zvm_neutron_agent, "LOG") as log: 147 | self.agent._treat_devices_added(['unknown_port']) 148 | log.debug.assert_called_with( 149 | "Device %s not defined on Neutron server", "unknown_port") 150 | 151 | def test_treat_devices_removed_returns_true_for_missing_device(self): 152 | attrs = {'update_device_down.side_effect': Exception()} 153 | self.agent.plugin_rpc.configure_mock(**attrs) 154 | devices = ['fake_uuid1'] 155 | with mock.patch.object(zvm_neutron_agent, "LOG") as log: 156 | self.agent._treat_devices_removed(devices) 157 | self.assertTrue(log.exception.called) 158 | 159 | def test_treat_devices_removed(self): 160 | devices = ['unknown_port', 'fake_uuid1'] 161 | with mock.patch.object(zvm_neutron_agent, "LOG") as log: 162 | self.agent._treat_devices_removed(devices) 163 | log.warn.assert_called_with('Can\'t find port %s in zvm agent', 164 | 'unknown_port') 165 | self.assertTrue(self.agent.plugin_rpc.update_device_down.called) 166 | 167 | def test_port_update_up(self): 168 | with mock.patch.object(self.agent.plugin_rpc, 169 | "update_device_up") as rpc: 170 | with mock.patch.object(self.agent._utils, 171 | "couple_nic_to_vswitch") as couple: 172 | self.agent.port_update(None, port={'id': 'fake_uuid1', 173 | 'admin_state_up': True}) 174 | self.assertTrue(rpc.called) 175 | self.assertTrue(couple.called) 176 | 177 | def test_port_update_down(self): 178 | with mock.patch.object(self.agent.plugin_rpc, 179 | "update_device_down") as rpc: 180 | with mock.patch.object(self.agent._utils, 181 | "uncouple_nic_from_vswitch") as couple: 182 | self.agent.port_update(None, port={'id': 'fake_uuid1', 183 | 'admin_state_up': False}) 184 | self.assertTrue(rpc.called) 185 | self.assertTrue(couple.called) 186 | 187 | # Test agent state report 188 | def test_report_state(self): 189 | with mock.patch.object(self.agent.state_rpc, 190 | "report_state") as report_st: 191 | self.agent._report_state() 192 | report_st.assert_called_with(self.agent.context, 193 | self.agent.agent_state) 194 | self.assertNotIn("start_flag", self.agent.agent_state) 195 | 196 | def test_treat_vif_port(self): 197 | with mock.patch.object(self.agent, "port_bound") as bound: 198 | self.agent._treat_vif_port('port_id', 'network_id', 'flat', 199 | 'vsw1', '10', True) 200 | self.assertTrue(bound.called) 201 | 202 | self.agent._treat_vif_port('port_id', 'network_id', 'flat', 203 | 'vsw1', '10', False) 204 | self.assertTrue(self.agent._utils.grant_user.called) 205 | 206 | def test_handle_restar_zvm(self): 207 | q_xcat = mock.MagicMock(return_value="xcat uptime 2") 208 | q_zvm = mock.MagicMock(return_value="zvm uptime 2") 209 | re_grant = mock.MagicMock() 210 | 211 | with mock.patch.multiple( 212 | self.agent._utils, 213 | query_xcat_uptime=q_xcat, 214 | query_zvm_uptime=q_zvm, 215 | re_grant_user=re_grant): 216 | self.agent._restart_handler.send(None) 217 | self.assertTrue(re_grant.called) 218 | self.assertTrue(self.agent._utils.create_xcat_mgt_network.called) 219 | 220 | def test_handle_restar_zvm_exception(self): 221 | q_xcat = mock.MagicMock(side_effect= 222 | Exception("xcat uptime exception")) 223 | with mock.patch.object(zvm_neutron_agent, "LOG") as log: 224 | with mock.patch.object(self.agent._utils, 225 | "query_xcat_uptime", q_xcat): 226 | self.agent._restart_handler.send(None) 227 | log.exception.assert_called_with("Failed to handle restart") 228 | -------------------------------------------------------------------------------- /neutron-zvm-plugin/neutron/tests/unit/zvm/test_zvm_utils.py: -------------------------------------------------------------------------------- 1 | # vim: tabstop=4 shiftwidth=4 softtabstop=4 2 | 3 | # Copyright 2014 IBM Corp. 4 | # 5 | # Licensed under the Apache License, Version 2.0 (the "License"); you may 6 | # not use this file except in compliance with the License. You may obtain 7 | # a copy of the License at 8 | # 9 | # http://www.apache.org/licenses/LICENSE-2.0 10 | # 11 | # Unless required by applicable law or agreed to in writing, software 12 | # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 13 | # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 14 | # License for the specific language governing permissions and limitations 15 | # under the License. 16 | 17 | """ 18 | Unit tests for the z/VM utils. 19 | """ 20 | 21 | import mock 22 | 23 | from neutron.plugins.zvm.common import exception 24 | from neutron.plugins.zvm.common import utils 25 | from neutron.tests import base 26 | from oslo.config import cfg 27 | 28 | 29 | class TestZVMUtils(base.BaseTestCase): 30 | 31 | _FAKE_VSWITCH_NAME = "fakevsw1" 32 | _FAKE_PORT_NAME = "fake_port_name" 33 | _FAKE_RET_VAL = 0 34 | _FAKE_VM_PATH = "fake_vm_path" 35 | _FAKE_VSWITCH = "fakevsw1" 36 | _FAKE_VLAN_ID = "fake_vlan_id" 37 | _FAKE_ZHCP_NODENAME = "fakezhcp" 38 | _FAKE_ZHCP_USER = 'zhcp_user' 39 | _FAKE_VDEV = "1000" 40 | _FAKE_XCAT_NODENAME = "fakexcat" 41 | _FAKE_XCAT_USER = "fake_xcat_user" 42 | _FAKE_XCAT_PW = "fake_xcat_password" 43 | 44 | def setUp(self): 45 | super(TestZVMUtils, self).setUp() 46 | self.addCleanup(cfg.CONF.reset) 47 | cfg.CONF.set_override('zvm_xcat_username', self._FAKE_XCAT_USER, 48 | group='AGENT') 49 | cfg.CONF.set_override('zvm_xcat_password', self._FAKE_XCAT_PW, 50 | group='AGENT') 51 | with mock.patch( 52 | 'neutron.plugins.zvm.common.utils.zvmUtils._get_xcat_node_name', 53 | mock.Mock(return_value=self._FAKE_XCAT_NODENAME)): 54 | self._utils = utils.zvmUtils() 55 | 56 | def test_couple_nic_to_vswitch(self): 57 | xcat_req = mock.Mock() 58 | xcat_req.side_effect = [{'data': [[self._FAKE_VDEV]]}, 59 | {'data': [['OK']]}, 60 | {'data': [['OK']]}] 61 | with mock.patch('neutron.plugins.zvm.common.xcatutils.xcat_request', 62 | xcat_req): 63 | ret = self._utils.couple_nic_to_vswitch(self._FAKE_VSWITCH, 64 | self._FAKE_PORT_NAME, 65 | self._FAKE_ZHCP_NODENAME, 66 | "fake_user") 67 | self.assertEqual(ret, self._FAKE_VDEV) 68 | 69 | url_vdev = ('/xcatws/tables/switch?userName=fake_xcat_user&' 70 | 'password=fake_xcat_password&format=json&' 71 | 'col=port&value=fake_port_name&attribute=interface') 72 | url_couple_nic = ('/xcatws/nodes/fakezhcp/dsh?userName=' 73 | 'fake_xcat_user&password=fake_xcat_password&format=json') 74 | body_couple_nic_dm = [('command=/opt/zhcp/bin/smcli' 75 | ' Virtual_Network_Adapter_Connect_Vswitch_DM -T fake_user' 76 | ' -v 1000 -n fakevsw1')] 77 | body_couple_nic = [('command=/opt/zhcp/bin/smcli' 78 | ' Virtual_Network_Adapter_Connect_Vswitch -T fake_user' 79 | ' -v 1000 -n fakevsw1')] 80 | 81 | calls = [mock.call('GET', url_vdev), 82 | mock.call('PUT', url_couple_nic, body_couple_nic_dm), 83 | mock.call('PUT', url_couple_nic, body_couple_nic)] 84 | xcat_req.assert_has_calls(calls) 85 | 86 | def test_grant_user(self): 87 | xcat_req = mock.Mock() 88 | with mock.patch('neutron.plugins.zvm.common.xcatutils.xcat_request', 89 | xcat_req): 90 | ret = self._utils.grant_user(self._FAKE_ZHCP_NODENAME, 91 | self._FAKE_VSWITCH, 92 | "fake_user") 93 | url_grant_user = ('/xcatws/nodes/fakezhcp/dsh?userName=' 94 | 'fake_xcat_user&password=fake_xcat_password&format=json') 95 | body_grant_user = [('command=/opt/zhcp/bin/smcli' 96 | ' Virtual_Network_Vswitch_Set_Extended -T fake_user' 97 | ' -k switch_name=fakevsw1 -k grant_userid=fake_user')] 98 | xcat_req.assert_called_with('PUT', url_grant_user, body_grant_user) 99 | 100 | def test_uncouple_nic_from_vswitch(self): 101 | xcat_req = mock.Mock() 102 | xcat_req.side_effect = [{'data': [[self._FAKE_VDEV]]}, 103 | {'data': [['OK']]}, 104 | {'data': [['OK']]}] 105 | with mock.patch('neutron.plugins.zvm.common.xcatutils.xcat_request', 106 | xcat_req): 107 | ret = self._utils.uncouple_nic_from_vswitch(self._FAKE_VSWITCH, 108 | self._FAKE_PORT_NAME, 109 | self._FAKE_ZHCP_NODENAME, 110 | "fake_user") 111 | 112 | url_vdev = ('/xcatws/tables/switch?userName=fake_xcat_user&' 113 | 'password=fake_xcat_password&format=json&' 114 | 'col=port&value=fake_port_name&attribute=interface') 115 | url_uncouple_nic = ('/xcatws/nodes/fakezhcp/dsh?userName=' 116 | 'fake_xcat_user&password=fake_xcat_password&format=json') 117 | body_uncouple_nic_dm = [('command=/opt/zhcp/bin/smcli' 118 | ' Virtual_Network_Adapter_Disconnect_DM -T fake_user' 119 | ' -v 1000')] 120 | body_uncouple_nic = [('command=/opt/zhcp/bin/smcli' 121 | ' Virtual_Network_Adapter_Disconnect -T fake_user -v 1000')] 122 | 123 | calls = [mock.call('GET', url_vdev), 124 | mock.call('PUT', url_uncouple_nic, body_uncouple_nic_dm), 125 | mock.call('PUT', url_uncouple_nic, body_uncouple_nic)] 126 | xcat_req.assert_has_calls(calls) 127 | 128 | def test_revoke_user(self): 129 | res = {'errorcode': [['0']]} 130 | xcat_req = mock.MagicMock() 131 | xcat_req.return_value = res 132 | with mock.patch('neutron.plugins.zvm.common.xcatutils.xcat_request', 133 | xcat_req): 134 | self._utils.revoke_user(self._FAKE_ZHCP_NODENAME, 135 | self._FAKE_VSWITCH_NAME, 136 | "fake_user") 137 | url_revoke_user = ('/xcatws/nodes/fakezhcp/dsh?userName=' 138 | 'fake_xcat_user&password=fake_xcat_password&format=json') 139 | body_revoke_user = [('command=/opt/zhcp/bin/smcli' 140 | ' Virtual_Network_Vswitch_Set_Extended -T fake_user' 141 | ' -k switch_name=fakevsw1 -k revoke_userid=fake_user')] 142 | xcat_req.assert_called_with('PUT', url_revoke_user, 143 | body_revoke_user) 144 | 145 | def test_add_vswitch_exist(self): 146 | res = {'errorcode': [['0']]} 147 | xcat_req = mock.MagicMock() 148 | xcat_req.return_value = res 149 | self._utils.get_zhcp_userid = mock.MagicMock( 150 | return_value=self._FAKE_ZHCP_USER) 151 | with mock.patch('neutron.plugins.zvm.common.xcatutils.xcat_request', 152 | xcat_req): 153 | with mock.patch.object(utils, "LOG") as log: 154 | self._utils.add_vswitch(self._FAKE_ZHCP_NODENAME, 155 | self._FAKE_VSWITCH_NAME, 156 | self._FAKE_VDEV) 157 | log.info.assert_called_with('Vswitch %s already exists.', 158 | self._FAKE_VSWITCH_NAME) 159 | 160 | def test_add_vswitch(self): 161 | self._utils.get_zhcp_userid = mock.MagicMock() 162 | self._utils.get_zhcp_userid.side_effect = [self._FAKE_ZHCP_USER, 163 | self._FAKE_ZHCP_USER, 164 | self._FAKE_ZHCP_USER] 165 | xcat_req = mock.Mock() 166 | res = {'errorcode': [['0']]} # vswitch does exist 167 | res_err = {'errorcode': [['1']]} # vswitch does not exist 168 | xcat_req.side_effect = [res_err, res, res] 169 | with mock.patch('neutron.plugins.zvm.common.xcatutils.xcat_request', 170 | xcat_req): 171 | self._utils.add_vswitch(self._FAKE_ZHCP_NODENAME, 172 | self._FAKE_VSWITCH_NAME, 173 | self._FAKE_VDEV, 174 | vid=[self._FAKE_VLAN_ID]) 175 | url = ('/xcatws/nodes/fakezhcp/dsh?userName=fake_xcat_user' 176 | '&password=fake_xcat_password&format=json') 177 | body = [('command=/opt/zhcp/bin/smcli' 178 | ' Virtual_Network_Vswitch_Create -T zhcp_user -n fakevsw1' 179 | ' -r 1000 -c 1 -q 8 -e 0 -t 2 -v 1' 180 | ' -p 1 -u 1 -G 2 -V 1')] 181 | xcat_req.assert_any_called('PUT', url, body) 182 | 183 | def test_set_vswitch_port_vlan_id(self): 184 | self._utils._get_nic_settings = mock.MagicMock(return_value='inst1') 185 | xcat_req = mock.Mock() 186 | xcat_req.return_value = "OK" 187 | with mock.patch('neutron.plugins.zvm.common.xcatutils.xcat_request', 188 | xcat_req): 189 | self._utils.set_vswitch_port_vlan_id(self._FAKE_VLAN_ID, 190 | self._FAKE_PORT_NAME, 191 | self._FAKE_VDEV, 192 | self._FAKE_ZHCP_NODENAME, 193 | self._FAKE_VSWITCH) 194 | url = ('/xcatws/nodes/fakezhcp/dsh?userName=fake_xcat_user' 195 | '&password=fake_xcat_password&format=json') 196 | body = [('command=/opt/zhcp/bin/smcli' 197 | ' Virtual_Network_Vswitch_Set_Extended -T inst1' 198 | ' -k grant_userid=inst1 -k switch_name=fakevsw1' 199 | ' -k user_vlan_id=fake_vlan_id')] 200 | xcat_req.assert_called_with('PUT', url, body) 201 | 202 | def test_get_nic_ids(self): 203 | xcat_req = mock.Mock() 204 | data = 'fnode,fswitch,fport,fvlan,finf,-,false' 205 | xcat_req.return_value = {'data': [[( 206 | '#node,switch,port,vlan,interface,comments,disable'), data]]} 207 | with mock.patch('neutron.plugins.zvm.common.xcatutils.xcat_request', 208 | xcat_req): 209 | ret = self._utils.get_nic_ids() 210 | self.assertEqual(ret, [data]) 211 | url = ('/xcatws/tables/switch?userName=fake_xcat_user&' 212 | 'password=fake_xcat_password&format=json') 213 | xcat_req.assert_called_with('GET', url) 214 | 215 | def test_get_node_from_port(self): 216 | xcat_req = mock.Mock() 217 | xcat_req.side_effect = [{'data': [[self._FAKE_ZHCP_NODENAME]]}] 218 | with mock.patch('neutron.plugins.zvm.common.xcatutils.xcat_request', 219 | xcat_req): 220 | ret = self._utils.get_node_from_port(self._FAKE_PORT_NAME) 221 | self.assertEqual(ret, self._FAKE_ZHCP_NODENAME) 222 | url = ('/xcatws/tables/switch?userName=fake_xcat_user&' 223 | 'password=fake_xcat_password&format=json&' 224 | 'col=port&value=fake_port_name&attribute=node') 225 | calls = [mock.call('GET', url)] 226 | xcat_req.assert_has_calls(calls) 227 | 228 | def _test_get_userid_from_node(self, node, user): 229 | xcat_req = mock.Mock() 230 | xcat_req.return_value = {'data': [[user]]} 231 | with mock.patch('neutron.plugins.zvm.common.xcatutils.xcat_request', 232 | xcat_req): 233 | ret = self._utils.get_zhcp_userid(self._FAKE_ZHCP_NODENAME) 234 | url = ('/xcatws/tables/zvm?userName=fake_xcat_user&' 235 | 'password=fake_xcat_password&format=json&col=node&' 236 | 'value=%s&attribute=userid' % node) 237 | xcat_req.assert_called_with('GET', url) 238 | return ret 239 | 240 | def test_get_userid_from_node(self): 241 | self.assertEqual(self._test_get_userid_from_node( 242 | self._FAKE_ZHCP_NODENAME, 243 | self._FAKE_ZHCP_USER), 244 | self._FAKE_ZHCP_USER) 245 | 246 | def test_get_zhcp_userid(self): 247 | self.assertEqual(self._test_get_userid_from_node( 248 | self._FAKE_ZHCP_NODENAME, 249 | self._FAKE_ZHCP_USER), 250 | self._FAKE_ZHCP_USER) 251 | 252 | def test_put_user_direct_online(self): 253 | xcat_req = mock.Mock() 254 | with mock.patch('neutron.plugins.zvm.common.xcatutils.xcat_request', 255 | xcat_req): 256 | self._utils.put_user_direct_online(self._FAKE_ZHCP_NODENAME, 257 | 'inst1') 258 | url = ('/xcatws/nodes/fakezhcp/dsh?userName=fake_xcat_user&' 259 | 'password=fake_xcat_password&format=json') 260 | body = [('command=/opt/zhcp/bin/smcli' 261 | ' Static_Image_Changes_Immediate_DM -T inst1')] 262 | xcat_req.assert_called_with('PUT', url, body) 263 | 264 | def test_update_xcat_switch(self): 265 | xcat_req = mock.Mock() 266 | with mock.patch('neutron.plugins.zvm.common.xcatutils.xcat_request', 267 | xcat_req): 268 | self._utils.update_xcat_switch(self._FAKE_PORT_NAME, 269 | self._FAKE_VSWITCH, 270 | self._FAKE_VLAN_ID) 271 | url = ('/xcatws/tables/switch?userName=fake_xcat_user&' 272 | 'password=fake_xcat_password&format=json') 273 | body = ['port=fake_port_name switch.switch=fakevsw1' 274 | ' switch.vlan=fake_vlan_id'] 275 | xcat_req.assert_called_with('PUT', url, body) 276 | 277 | def _verify_query_nic(self, result, xcat_req): 278 | url = ('/xcatws/nodes/fakexcat/dsh?userName=fake_xcat_user&' 279 | 'password=fake_xcat_password&format=json') 280 | body = ['command=smcli Virtual_Network_Adapter_Query' 281 | ' -T fakexcat -v 0800'] 282 | xcat_req.assert_any_with('PUT', url, body) 283 | 284 | def test_create_xcat_mgt_network_exist(self): 285 | nic_def = ['zhcp: Adapter:\nzhcp: Address: 0800\n' 286 | 'zhcp: Device count: 3\nzhcp: Adapter type: QDIO\n' 287 | 'zhcp: Adapter status: Coupled and active\n' 288 | 'zhcp: LAN owner: SYSTEM\n' 289 | 'zhcp: LAN name: XCATVSW2'] 290 | xcat_req = mock.Mock() 291 | xcat_req.side_effect = [{'data': [nic_def]}, 292 | {'data': [['OK']]}] 293 | with mock.patch('neutron.plugins.zvm.common.xcatutils.xcat_request', 294 | xcat_req): 295 | self._utils.create_xcat_mgt_network(self._FAKE_ZHCP_NODENAME, 296 | "10.1.1.1", 297 | "255.255.0.0", 298 | self._FAKE_VSWITCH) 299 | self._verify_query_nic(nic_def, xcat_req) 300 | url = ('/xcatws/nodes/fakexcat/dsh?userName=fake_xcat_user&' 301 | 'password=fake_xcat_password&format=json') 302 | body = ['command=/usr/bin/perl /usr/sbin/sspqeth2.pl -a 10.1.1.1' 303 | ' -d 0800 0801 0802 -e eth2 -m 255.255.0.0 -g 10.1.1.1'] 304 | xcat_req.assert_called_with('PUT', url, body) 305 | 306 | def test_create_xcat_mgt_network_not_exist(self): 307 | nic_undef = ['zhcp: Failed\nzhcp: Return Code: 212\n' 308 | 'zhcp: Reason Code: 8\n' 309 | 'zhcp: Description: Adapter does not exist'] 310 | xcat_req = mock.Mock() 311 | xcat_req.side_effect = [{'data': [nic_undef]}, 312 | {'data': [['OK']]}] 313 | with mock.patch('neutron.plugins.zvm.common.xcatutils.xcat_request', 314 | xcat_req): 315 | self._utils.create_xcat_mgt_network(self._FAKE_ZHCP_NODENAME, 316 | "10.1.1.1", 317 | "255.255.0.0", 318 | self._FAKE_VSWITCH) 319 | self._verify_query_nic(nic_undef, xcat_req) 320 | url = ('/xcatws/nodes/fakexcat/dsh?userName=fake_xcat_user&' 321 | 'password=fake_xcat_password&format=json') 322 | body = ['command=vmcp define nic 0800 type qdio\n' 323 | 'vmcp couple 0800 system fakevsw1\n' 324 | '/usr/bin/perl /usr/sbin/sspqeth2.pl -a 10.1.1.1' 325 | ' -d 0800 0801 0802 -e eth2 -m 255.255.0.0 -g 10.1.1.1'] 326 | xcat_req.assert_called_with('PUT', url, body) 327 | 328 | def test_create_xcat_mgt_network_error(self): 329 | nic_err = ['zhcp: Adapter:\nzhcp: Address: 0800\n' 330 | 'zhcp: Device count: 3\nzhcp: Adapter type: QDIO\n' 331 | 'zhcp: Adapter status: Not coupled\n' 332 | 'zhcp: LAN owner: \n' 333 | 'zhcp: LAN name: '] 334 | smapi_err = ['Failed'] 335 | xcat_req = mock.Mock() 336 | xcat_req.side_effect = [{'data': [nic_err]}, 337 | {'data': [smapi_err]}] 338 | with mock.patch('neutron.plugins.zvm.common.xcatutils.xcat_request', 339 | xcat_req): 340 | with mock.patch.object(utils, "LOG") as log: 341 | self._utils.create_xcat_mgt_network(self._FAKE_ZHCP_NODENAME, 342 | "10.1.1.1", 343 | "255.255.0.0", 344 | self._FAKE_VSWITCH) 345 | self._verify_query_nic(nic_err, xcat_req) 346 | log.error.assert_called_with('NIC 800 staus is unknown.') 347 | 348 | self.assertRaises(exception.zvmException, 349 | self._utils.create_xcat_mgt_network, 350 | self._FAKE_ZHCP_NODENAME, 351 | "10.1.1.1", 352 | "255.255.0.0", 353 | self._FAKE_VSWITCH) 354 | self._verify_query_nic(smapi_err, xcat_req) 355 | 356 | def test_re_grant_user(self): 357 | '''We assume there is three nodes valid in the xCAT MN db, they are: 358 | node1, node2, node4. We mock _MAX_REGRANT_USER_NUMBER to 2. So the 359 | process of regrant has two steps. Fisrt grant two nodes and then 360 | grant one node.''' 361 | 362 | fake_port_info = ['node1,switch,port1,10,inf1,fakezhcp,false', 363 | 'node2,switch,port2,10,inf2,fakezhcp,false', 364 | # node3's zhcp field is invalid 365 | 'node3,switch,port3,10,inf3,zhcp,false', 366 | 'node4,switch,port3,10,inf4,fakezhcp,false'] 367 | self._utils.get_nic_ids = mock.MagicMock(return_value=fake_port_info) 368 | 369 | fake_user_id = ['#node,hcp,userid,nodetype,parent,comments,disable', 370 | '"opnstk1","zhcp.ibm.com",,,,,', # invalid record 371 | '"node1","fakezhcp","user01",,,,', 372 | '"zhcp2","zhcp2.ibm.com","ZHCP",,,,', # invalid record 373 | '"node2","fakezhcp","user02",,,,', 374 | '"node3","zhcp","user03",,,,', # invalid record 375 | '"node4","fakezhcp","user04",,,,'] 376 | 377 | xcat_req = mock.Mock() 378 | xcat_req.side_effect = [{'data': [fake_user_id]}, 379 | {'data': [['OK']]}, # run_command step 1, regrant two node 380 | {'data': [['OK']]}, # run_command remove 381 | {'data': [['OK']]}, # run_command step 2 382 | {'data': [['OK']]}] # run_command remove 383 | 384 | with mock.patch.object(utils.zvmUtils, '_MAX_REGRANT_USER_NUMBER', 2): 385 | with mock.patch( 386 | 'neutron.plugins.zvm.common.xcatutils.xcat_request', 387 | xcat_req): 388 | self._utils.re_grant_user(self._FAKE_ZHCP_NODENAME) 389 | url_command = ('/xcatws/nodes/fakezhcp/dsh?userName=' 390 | 'fake_xcat_user&password=fake_xcat_password&format=json') 391 | 392 | valid_users = [1, 2, 4] 393 | last_user = None 394 | 395 | # re_grant_user uses a dict to keep the ports info, so we don't 396 | # know the order, which nodes are reganted in step 1 and which 397 | # one is regranted in step 2. We will try to find the node 398 | # removed in step 2 first, because this is easier. Then we 399 | # verify the step 1. 400 | for i in valid_users: 401 | cmd_vsw_couple =\ 402 | ('command=echo -e "#!/bin/sh\n/opt/zhcp/bin/smcli' 403 | ' Virtual_Network_Vswitch_Set_Extended -T user0%s -k' 404 | ' switch_name=switch -k grant_userid=user0%s' 405 | ' -k user_vlan_id=10" > grant.sh' % (i, i)) 406 | if mock.call('PUT', url_command, [cmd_vsw_couple]) in\ 407 | xcat_req.call_args_list: 408 | last_user = i 409 | break 410 | self.assertTrue(last_user) 411 | # remove the node from valid users, so we can verify if the 412 | # other two nodes has been regranted via the valid_users. 413 | del(valid_users[valid_users.index(last_user)]) 414 | 415 | body_cmd_node_1 =\ 416 | ('command=echo -e "#!/bin/sh\n' 417 | '/opt/zhcp/bin/smcli Virtual_Network_Vswitch_Set_Extended' 418 | ' -T user0%s -k switch_name=switch -k grant_userid=user0%s' 419 | ' -k user_vlan_id=10\n' 420 | % (valid_users[0], valid_users[0])) +\ 421 | ('/opt/zhcp/bin/smcli Virtual_Network_Vswitch_Set_Extended' 422 | ' -T user0%s -k switch_name=switch -k grant_userid=user0%s' 423 | ' -k user_vlan_id=10" > grant.sh' 424 | % (valid_users[1], valid_users[1])) 425 | 426 | body_cmd_node_2 =\ 427 | ('command=echo -e "#!/bin/sh\n' 428 | '/opt/zhcp/bin/smcli Virtual_Network_Vswitch_Set_Extended' 429 | ' -T user0%s -k switch_name=switch -k grant_userid=user0%s' 430 | ' -k user_vlan_id=10\n' 431 | % (valid_users[1], valid_users[1])) +\ 432 | ('/opt/zhcp/bin/smcli Virtual_Network_Vswitch_Set_Extended' 433 | ' -T user0%s -k switch_name=switch -k grant_userid=user0%s' 434 | ' -k user_vlan_id=10" > grant.sh' 435 | % (valid_users[0], valid_users[0])) 436 | self.assertTrue( 437 | (mock.call('PUT', url_command, [body_cmd_node_1]) 438 | in xcat_req.call_args_list) 439 | or (mock.call('PUT', url_command, [body_cmd_node_2]) 440 | in xcat_req.call_args_list)) 441 | 442 | def test_query_xcat_uptime(self): 443 | xcat_uptime = {'data': 444 | [['XCAT was activated on 2014-06-11 at 02:41:15']]} 445 | xcat_req = mock.Mock(return_value=xcat_uptime) 446 | with mock.patch('neutron.plugins.zvm.common.xcatutils.xcat_request', 447 | xcat_req): 448 | with mock.patch.object(utils.zvmUtils, "get_userid_from_node", 449 | mock.Mock(return_value='xcat')): 450 | ret = self._utils.query_xcat_uptime(self._FAKE_ZHCP_NODENAME) 451 | self.assertEqual(ret, '2014-06-11 at 02:41:15') 452 | url = ('/xcatws/nodes/fakezhcp/dsh?userName=fake_xcat_user&' 453 | 'password=fake_xcat_password&format=json') 454 | body = ['command=/opt/zhcp/bin/smcli' 455 | ' Image_Query_Activate_Time -T xcat -f 4'] 456 | xcat_req.assert_called_with('PUT', url, body) 457 | 458 | def test_query_zvm_uptime(self): 459 | fake_ret = ('timezone\ncurrent time\nversion\nGen time\n' 460 | 'zhcp: The z/VM CP IPL time: 2014-06-11 01:38:37 EDT\n' 461 | 'storage\n') 462 | zvm_uptime = {'data': [[fake_ret]]} 463 | xcat_req = mock.Mock(return_value=zvm_uptime) 464 | with mock.patch('neutron.plugins.zvm.common.xcatutils.xcat_request', 465 | xcat_req): 466 | ret = self._utils.query_zvm_uptime(self._FAKE_ZHCP_NODENAME) 467 | self.assertEqual(ret, '2014-06-11 01:38:37 EDT') 468 | url = ('/xcatws/nodes/fakezhcp/dsh?userName=fake_xcat_user&' 469 | 'password=fake_xcat_password&format=json') 470 | body = ['command=/opt/zhcp/bin/smcli System_Info_Query'] 471 | xcat_req.assert_called_with('PUT', url, body) 472 | -------------------------------------------------------------------------------- /neutron-zvm-plugin/neutron/tests/unit/zvm/test_zvm_xcatutils.py: -------------------------------------------------------------------------------- 1 | # vim: tabstop=4 shiftwidth=4 softtabstop=4 2 | 3 | # Copyright 2014 IBM Corp. 4 | # 5 | # Licensed under the Apache License, Version 2.0 (the "License"); you may 6 | # not use this file except in compliance with the License. You may obtain 7 | # a copy of the License at 8 | # 9 | # http://www.apache.org/licenses/LICENSE-2.0 10 | # 11 | # Unless required by applicable law or agreed to in writing, software 12 | # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 13 | # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 14 | # License for the specific language governing permissions and limitations 15 | # under the License. 16 | 17 | """ 18 | Unit tests for the z/VM xCAT utils. 19 | """ 20 | 21 | import mock 22 | 23 | from oslo.config import cfg 24 | from neutron.plugins.zvm.common import xcatutils 25 | from neutron.tests import base 26 | 27 | 28 | class TestZVMXcatUtils(base.BaseTestCase): 29 | _FAKE_XCAT_SERVER = "127.0.0.1" 30 | _FAKE_XCAT_TIMEOUT = 300 31 | 32 | def setUp(self): 33 | super(TestZVMXcatUtils, self).setUp() 34 | cfg.CONF.set_override('zvm_xcat_server', 35 | self._FAKE_XCAT_SERVER, 'AGENT') 36 | cfg.CONF.set_override('zvm_xcat_timeout', 37 | self._FAKE_XCAT_TIMEOUT, 'AGENT') 38 | self._xcaturl = xcatutils.xCatURL() 39 | with mock.patch.multiple(xcatutils.httplib, 40 | HTTPSConnection=mock.MagicMock()): 41 | self._zvm_xcat_connection = xcatutils.xCatConnection() 42 | -------------------------------------------------------------------------------- /nova-zvm-virt-driver/nova/virt/zvm/__init__.py: -------------------------------------------------------------------------------- 1 | # vim: tabstop=4 shiftwidth=4 softtabstop=4 2 | 3 | # Copyright 2013 IBM Corp. 4 | # 5 | # Licensed under the Apache License, Version 2.0 (the "License"); you may 6 | # not use this file except in compliance with the License. You may obtain 7 | # a copy of the License at 8 | # 9 | # http://www.apache.org/licenses/LICENSE-2.0 10 | # 11 | # Unless required by applicable law or agreed to in writing, software 12 | # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 13 | # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 14 | # License for the specific language governing permissions and limitations 15 | # under the License. 16 | 17 | """A connection to an IBM z/VM Virtualization system. 18 | 19 | Generally, OpenStack z/VM virt driver will call xCat REST API to operate 20 | to z/VM hypervisors.xCat has a control point(a virtual machine) in z/VM 21 | system, which enables xCat management node to control the z/VM system. 22 | OpenStack z/VM driver will communicate with xCat management node through 23 | xCat REST API. Thus OpenStack can operate to z/VM system indirectly. 24 | 25 | """ 26 | 27 | 28 | from nova.virt.zvm import driver 29 | 30 | 31 | ZVMDriver = driver.ZVMDriver 32 | -------------------------------------------------------------------------------- /nova-zvm-virt-driver/nova/virt/zvm/configdrive.py: -------------------------------------------------------------------------------- 1 | # vim: tabstop=4 shiftwidth=4 softtabstop=4 2 | 3 | # Copyright 2013 IBM Corp. 4 | # 5 | # Licensed under the Apache License, Version 2.0 (the "License"); you may 6 | # not use this file except in compliance with the License. You may obtain 7 | # a copy of the License at 8 | # 9 | # http://www.apache.org/licenses/LICENSE-2.0 10 | # 11 | # Unless required by applicable law or agreed to in writing, software 12 | # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 13 | # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 14 | # License for the specific language governing permissions and limitations 15 | # under the License. 16 | 17 | import os 18 | import tarfile 19 | 20 | from oslo.config import cfg 21 | 22 | from nova import exception 23 | from nova import utils 24 | from nova.virt import configdrive 25 | 26 | 27 | CONF = cfg.CONF 28 | 29 | 30 | class ZVMConfigDriveBuilder(configdrive.ConfigDriveBuilder): 31 | """Enable ConfigDrive to make tgz package.""" 32 | 33 | def __init__(self, instance_md): 34 | super(ZVMConfigDriveBuilder, self).__init__(instance_md) 35 | 36 | def make_drive(self, path): 37 | """Make the config drive. 38 | 39 | :param path: the path to place the config drive image at 40 | :raises ProcessExecuteError if a helper process has failed. 41 | 42 | """ 43 | if CONF.config_drive_format == 'tgz': 44 | self._make_tgz(path) 45 | else: 46 | raise exception.ConfigDriveUnknownFormat( 47 | format=CONF.config_drive_format) 48 | 49 | def _make_tgz(self, path): 50 | try: 51 | olddir = os.getcwd() 52 | except OSError: 53 | olddir = CONF.state_path 54 | 55 | with utils.tempdir() as tmpdir: 56 | self._write_md_files(tmpdir) 57 | tar = tarfile.open(path, "w:gz") 58 | os.chdir(tmpdir) 59 | tar.add("openstack") 60 | tar.add("ec2") 61 | try: 62 | os.chdir(olddir) 63 | except Exception: 64 | pass 65 | tar.close() 66 | -------------------------------------------------------------------------------- /nova-zvm-virt-driver/nova/virt/zvm/const.py: -------------------------------------------------------------------------------- 1 | # vim: tabstop=4 shiftwidth=4 softtabstop=4 2 | 3 | # Copyright 2013 IBM Corp. 4 | # 5 | # Licensed under the Apache License, Version 2.0 (the "License"); you may 6 | # not use this file except in compliance with the License. You may obtain 7 | # a copy of the License at 8 | # 9 | # http://www.apache.org/licenses/LICENSE-2.0 10 | # 11 | # Unless required by applicable law or agreed to in writing, software 12 | # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 13 | # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 14 | # License for the specific language governing permissions and limitations 15 | # under the License. 16 | 17 | 18 | from nova.compute import power_state 19 | 20 | 21 | HYPERVISOR_TYPE = 'zvm' 22 | ARCHITECTURE = 's390x' 23 | ALLOWED_VM_TYPE = 'zLinux' 24 | XCAT_MGT = 'zvm' 25 | 26 | XCAT_RINV_HOST_KEYWORDS = { 27 | "zvm_host": "z/VM Host:", 28 | "zhcp": "zHCP:", 29 | "cec_vendor": "CEC Vendor:", 30 | "cec_model": "CEC Model:", 31 | "hypervisor_os": "Hypervisor OS:", 32 | "hypervisor_name": "Hypervisor Name:", 33 | "architecture": "Architecture:", 34 | "lpar_cpu_total": "LPAR CPU Total:", 35 | "lpar_cpu_used": "LPAR CPU Used:", 36 | "lpar_memory_total": "LPAR Memory Total:", 37 | "lpar_memory_used": "LPAR Memory Used:", 38 | "lpar_memory_offline": "LPAR Memory Offline:", 39 | "ipl_time": "IPL Time:", 40 | } 41 | 42 | XCAT_DISKPOOL_KEYWORDS = { 43 | "disk_total": "Total:", 44 | "disk_used": "Used:", 45 | "disk_available": "Free:", 46 | } 47 | 48 | XCAT_RESPONSE_KEYS = ('info', 'data', 'node', 'errorcode', 'error') 49 | 50 | ZVM_POWER_STAT = { 51 | 'on': power_state.RUNNING, 52 | 'off': power_state.SHUTDOWN, 53 | } 54 | 55 | ZVM_DEFAULT_ROOT_DISK = "dasda" 56 | ZVM_DEFAULT_SECOND_DISK = "dasdb" 57 | ZVM_DEFAULT_ROOT_VOLUME = "sda" 58 | ZVM_DEFAULT_SECOND_VOLUME = "sdb" 59 | ZVM_DEFAULT_THIRD_VOLUME = "sdc" 60 | ZVM_DEFAULT_LAST_VOLUME = "sdz" 61 | 62 | DEFAULT_EPH_DISK_FMT = "ext3" 63 | 64 | ZVM_DEFAULT_FCP_ID = 'auto' 65 | 66 | ZVM_DEFAULT_NIC_VDEV = '1000' 67 | 68 | ZVM_IMAGE_SIZE_MAX = 10 69 | -------------------------------------------------------------------------------- /nova-zvm-virt-driver/nova/virt/zvm/exception.py: -------------------------------------------------------------------------------- 1 | # vim: tabstop=4 shiftwidth=4 softtabstop=4 2 | 3 | # Copyright 2013 IBM Corp. 4 | # 5 | # Licensed under the Apache License, Version 2.0 (the "License"); you may 6 | # not use this file except in compliance with the License. You may obtain 7 | # a copy of the License at 8 | # 9 | # http://www.apache.org/licenses/LICENSE-2.0 10 | # 11 | # Unless required by applicable law or agreed to in writing, software 12 | # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 13 | # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 14 | # License for the specific language governing permissions and limitations 15 | # under the License. 16 | 17 | 18 | from nova import exception 19 | from nova.openstack.common.gettextutils import _ 20 | 21 | 22 | class ZVMBaseException(exception.NovaException): 23 | """Base z/VM exception.""" 24 | pass 25 | 26 | 27 | class ZVMDriverError(ZVMBaseException): 28 | msg_fmt = _('z/VM driver error: %(msg)s') 29 | 30 | 31 | class ZVMXCATRequestFailed(ZVMBaseException): 32 | msg_fmt = _('Request to xCAT server %(xcatserver)s failed: %(msg)s') 33 | 34 | 35 | class ZVMInvalidXCATResponseDataError(ZVMBaseException): 36 | msg_fmt = _('Invalid data returned from xCAT: %(msg)s') 37 | 38 | 39 | class ZVMXCATInternalError(ZVMBaseException): 40 | msg_fmt = _('Error returned from xCAT: %(msg)s') 41 | 42 | 43 | class ZVMVolumeError(ZVMBaseException): 44 | msg_fmt = _('Volume error: %(msg)s') 45 | 46 | 47 | class ZVMImageError(ZVMBaseException): 48 | msg_fmt = _("Image error: %(msg)s") 49 | 50 | 51 | class ZVMGetImageFromXCATFailed(ZVMBaseException): 52 | msg_fmt = _('Get image from xCAT failed: %(msg)s') 53 | 54 | 55 | class ZVMNetworkError(ZVMBaseException): 56 | msg_fmt = _("z/VM network error: %(msg)s") 57 | 58 | 59 | class ZVMXCATXdshFailed(ZVMBaseException): 60 | msg_fmt = _('Execute xCAT xdsh command failed: %(msg)s') 61 | 62 | 63 | class ZVMXCATCreateNodeFailed(ZVMBaseException): 64 | msg_fmt = _('Create xCAT node %(node)s failed: %(msg)s') 65 | 66 | 67 | class ZVMXCATCreateUserIdFailed(ZVMBaseException): 68 | msg_fmt = _('Create xCAT user id %(instance)s failed: %(msg)s') 69 | 70 | 71 | class ZVMXCATUpdateNodeFailed(ZVMBaseException): 72 | msg_fmt = _('Update node %(node)s info failed: %(msg)s') 73 | 74 | 75 | class ZVMXCATDeployNodeFailed(ZVMBaseException): 76 | msg_fmt = _('Deploy image on node %(node)s failed: %(msg)s') 77 | 78 | 79 | class ZVMConfigDriveError(ZVMBaseException): 80 | msg_fmt = _('Create configure drive failed: %(msg)s') 81 | -------------------------------------------------------------------------------- /nova-zvm-virt-driver/nova/virt/zvm/instance.py: -------------------------------------------------------------------------------- 1 | # vim: tabstop=4 shiftwidth=4 softtabstop=4 2 | 3 | # Copyright 2013 IBM Corp. 4 | # 5 | # Licensed under the Apache License, Version 2.0 (the "License"); you may 6 | # not use this file except in compliance with the License. You may obtain 7 | # a copy of the License at 8 | # 9 | # http://www.apache.org/licenses/LICENSE-2.0 10 | # 11 | # Unless required by applicable law or agreed to in writing, software 12 | # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 13 | # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 14 | # License for the specific language governing permissions and limitations 15 | # under the License. 16 | 17 | 18 | import datetime 19 | from oslo.config import cfg 20 | 21 | from nova.compute import power_state 22 | from nova import exception as nova_exception 23 | from nova.openstack.common.gettextutils import _ 24 | from nova.openstack.common import log as logging 25 | from nova.openstack.common import loopingcall 26 | from nova.openstack.common import timeutils 27 | from nova.virt.zvm import const 28 | from nova.virt.zvm import exception 29 | from nova.virt.zvm import utils as zvmutils 30 | 31 | LOG = logging.getLogger(__name__) 32 | CONF = cfg.CONF 33 | 34 | 35 | class ZVMInstance(object): 36 | '''OpenStack instance that running on of z/VM hypervisor.''' 37 | 38 | def __init__(self, instance={}): 39 | """Initialize instance attributes for database.""" 40 | self._xcat_url = zvmutils.XCATUrl() 41 | self._xcat_conn = zvmutils.XCATConnection() 42 | self._instance = instance 43 | self._name = instance['name'] 44 | 45 | def power_off(self): 46 | """Power off z/VM instance.""" 47 | try: 48 | self._power_state("PUT", "off") 49 | except exception.ZVMXCATInternalError as err: 50 | err_str = err.format_message() 51 | if ("Return Code: 200" in err_str and 52 | "Reason Code: 12" in err_str): 53 | # Instance already not active 54 | LOG.warn(_("z/VM instance %s not active") % self._name) 55 | return 56 | else: 57 | msg = _("Failed to power off instance: %s") % err 58 | LOG.error(msg) 59 | raise nova_exception.InstancePowerOffFailure(reason=msg) 60 | 61 | def power_on(self): 62 | """"Power on z/VM instance.""" 63 | try: 64 | self._power_state("PUT", "on") 65 | except exception.ZVMXCATInternalError as err: 66 | err_str = err.format_message() 67 | if ("Return Code: 200" in err_str and 68 | "Reason Code: 8" in err_str): 69 | # Instance already not active 70 | LOG.warn(_("z/VM instance %s already active") % self._name) 71 | return 72 | 73 | self._wait_for_reachable() 74 | if not self._reachable: 75 | LOG.error(_("Failed to power on instance %s: timeout") % 76 | self._name) 77 | raise nova_exception.InstancePowerOnFailure(reason="timeout") 78 | 79 | def reset(self): 80 | """Hard reboot z/VM instance.""" 81 | try: 82 | self._power_state("PUT", "reset") 83 | except exception.ZVMXCATInternalError as err: 84 | err_str = err.format_message() 85 | if ("Return Code: 200" in err_str and 86 | "Reason Code: 12" in err_str): 87 | # Be able to reset in power state of SHUTDOWN 88 | LOG.warn(_("Reset z/VM instance %s from SHUTDOWN state") % 89 | self._name) 90 | return 91 | else: 92 | raise err 93 | self._wait_for_reachable() 94 | 95 | def reboot(self): 96 | """Soft reboot z/VM instance.""" 97 | self._power_state("PUT", "reboot") 98 | self._wait_for_reachable() 99 | 100 | def pause(self): 101 | """Pause the z/VM instance.""" 102 | self._power_state("PUT", "pause") 103 | 104 | def unpause(self): 105 | """Unpause the z/VM instance.""" 106 | self._power_state("PUT", "unpause") 107 | self._wait_for_reachable() 108 | 109 | def attach_volume(self, volumeop, context, connection_info, instance, 110 | mountpoint, is_active, rollback=True): 111 | volumeop.attach_volume_to_instance(context, connection_info, 112 | instance, mountpoint, 113 | is_active, rollback) 114 | 115 | def detach_volume(self, volumeop, connection_info, instance, mountpoint, 116 | is_active, rollback=True): 117 | volumeop.detach_volume_from_instance(connection_info, 118 | instance, mountpoint, 119 | is_active, rollback) 120 | 121 | def get_info(self): 122 | """Get the current status of an z/VM instance. 123 | 124 | Returns a dict containing: 125 | 126 | :state: the running state, one of the power_state codes 127 | :max_mem: (int) the maximum memory in KBytes allowed 128 | :mem: (int) the memory in KBytes used by the domain 129 | :num_cpu: (int) the number of virtual CPUs for the domain 130 | :cpu_time: (int) the CPU time used in nanoseconds 131 | 132 | """ 133 | power_stat = self._get_power_stat() 134 | is_reachable = self.is_reachable() 135 | 136 | max_mem_kb = int(self._instance['memory_mb']) * 1024 137 | if is_reachable: 138 | try: 139 | rec_list = self._get_rinv_info() 140 | except exception.ZVMXCATInternalError: 141 | raise nova_exception.InstanceNotFound(instance_id=self._name) 142 | 143 | try: 144 | mem = self._get_current_memory(rec_list) 145 | num_cpu = self._get_cpu_count(rec_list) 146 | cpu_time = self._get_cpu_used_time(rec_list) 147 | _instance_info = {'state': power_stat, 148 | 'max_mem': max_mem_kb, 149 | 'mem': mem, 150 | 'num_cpu': num_cpu, 151 | 'cpu_time': cpu_time, } 152 | 153 | except exception.ZVMInvalidXCATResponseDataError: 154 | LOG.warn(_("Failed to get inventory info for %s") % self._name) 155 | _instance_info = {'state': power_stat, 156 | 'max_mem': max_mem_kb, 157 | 'mem': max_mem_kb, 158 | 'num_cpu': self._instance['vcpus'], 159 | 'cpu_time': 0, } 160 | 161 | else: 162 | # Since xCAT rinv can't get info from a server that in power state 163 | # of SHUTDOWN or PAUSED 164 | if ((power_stat == power_state.RUNNING) and 165 | (self._instance['power_state'] == power_state.PAUSED)): 166 | # return paused state only previous power state is paused 167 | _instance_info = {'state': power_state.PAUSED, 168 | 'max_mem': max_mem_kb, 169 | 'mem': max_mem_kb, 170 | 'num_cpu': self._instance['vcpus'], 171 | 'cpu_time': 0, } 172 | else: 173 | # otherwise return xcat returned state 174 | _instance_info = {'state': power_stat, 175 | 'max_mem': max_mem_kb, 176 | 'mem': 0, 177 | 'num_cpu': self._instance['vcpus'], 178 | 'cpu_time': 0, } 179 | return _instance_info 180 | 181 | def create_xcat_node(self, zhcp, userid=None): 182 | """Create xCAT node for z/VM instance.""" 183 | LOG.debug(_("Creating xCAT node for %s") % self._name) 184 | 185 | user_id = userid or self._name 186 | body = ['userid=%s' % user_id, 187 | 'hcp=%s' % zhcp, 188 | 'mgt=zvm', 189 | 'groups=%s' % CONF.zvm_xcat_group] 190 | url = self._xcat_url.mkdef('/' + self._name) 191 | 192 | with zvmutils.except_xcat_call_failed_and_reraise( 193 | exception.ZVMXCATCreateNodeFailed, node=self._name): 194 | zvmutils.xcat_request("POST", url, body) 195 | 196 | def create_userid(self, block_device_info, image_meta): 197 | """Create z/VM userid into user directory for a z/VM instance.""" 198 | # We do not support boot from volume currently 199 | LOG.debug(_("Creating the z/VM user entry for instance %s") 200 | % self._name) 201 | is_volume_base = zvmutils.volume_in_mapping( 202 | const.ZVM_DEFAULT_ROOT_VOLUME, block_device_info) 203 | if is_volume_base: 204 | # TODO(rui): Boot from volume 205 | msg = _("Not support boot from volume.") 206 | raise exception.ZVMXCATCreateUserIdFailed(instance=self._name, 207 | msg=msg) 208 | 209 | eph_disks = block_device_info.get('ephemerals', []) 210 | kwprofile = 'profile=%s' % CONF.zvm_user_profile 211 | body = [kwprofile, 212 | 'password=%s' % CONF.zvm_user_default_password, 213 | 'cpu=%i' % self._instance['vcpus'], 214 | 'memory=%im' % self._instance['memory_mb'], 215 | 'privilege=%s' % CONF.zvm_user_default_privilege] 216 | url = self._xcat_url.mkvm('/' + self._name) 217 | 218 | try: 219 | zvmutils.xcat_request("POST", url, body) 220 | 221 | if not is_volume_base: 222 | size = '%ig' % self._instance['root_gb'] 223 | # use a flavor the disk size is 0 224 | if size == '0g': 225 | size = image_meta['properties']['root_disk_units'] 226 | # Add root disk and set ipl 227 | self.add_mdisk(CONF.zvm_diskpool, 228 | CONF.zvm_user_root_vdev, 229 | size) 230 | self._set_ipl(CONF.zvm_user_root_vdev) 231 | 232 | # Add additional ephemeral disk 233 | if self._instance['ephemeral_gb'] != 0: 234 | if eph_disks == []: 235 | # Create ephemeral disk according to flavor 236 | fmt = (CONF.default_ephemeral_format or 237 | const.DEFAULT_EPH_DISK_FMT) 238 | self.add_mdisk(CONF.zvm_diskpool, 239 | CONF.zvm_user_adde_vdev, 240 | '%ig' % self._instance['ephemeral_gb'], 241 | fmt) 242 | else: 243 | # Create ephemeral disks according --ephemeral option 244 | for idx, eph in enumerate(eph_disks): 245 | vdev = (eph.get('vdev') or 246 | zvmutils.generate_eph_vdev(idx)) 247 | size = eph['size'] 248 | size_in_units = eph.get('size_in_units', False) 249 | if not size_in_units: 250 | size = '%ig' % size 251 | fmt = (eph.get('guest_format') or 252 | CONF.default_ephemeral_format or 253 | const.DEFAULT_EPH_DISK_FMT) 254 | self.add_mdisk(CONF.zvm_diskpool, vdev, size, fmt) 255 | except (exception.ZVMXCATRequestFailed, 256 | exception.ZVMInvalidXCATResponseDataError, 257 | exception.ZVMXCATInternalError, 258 | exception.ZVMDriverError) as err: 259 | msg = _("Failed to create z/VM userid: %s") % err 260 | LOG.error(msg) 261 | raise exception.ZVMXCATCreateUserIdFailed(instance=self._name, 262 | msg=msg) 263 | 264 | def _set_ipl(self, ipl_state): 265 | body = ["--setipl %s" % ipl_state] 266 | url = self._xcat_url.chvm('/' + self._name) 267 | zvmutils.xcat_request("PUT", url, body) 268 | 269 | def is_locked(self, zhcp_node): 270 | cmd = "smcli Image_Lock_Query_DM -T %s" % self._name 271 | resp = zvmutils.xdsh(zhcp_node, cmd) 272 | 273 | return "is Unlocked..." not in str(resp) 274 | 275 | def _wait_for_unlock(self, zhcp_node, interval=10, timeout=600): 276 | LOG.debug("Waiting for unlock instance %s" % self._name) 277 | 278 | def _wait_unlock(expiration): 279 | if timeutils.utcnow() > expiration: 280 | LOG.debug("Waiting for unlock instance %s timeout" % 281 | self._name) 282 | raise loopingcall.LoopingCallDone() 283 | 284 | if not self.is_locked(zhcp_node): 285 | LOG.debug("Instance %s is unlocked" % 286 | self._name) 287 | raise loopingcall.LoopingCallDone() 288 | 289 | expiration = timeutils.utcnow() + datetime.timedelta(seconds=timeout) 290 | 291 | timer = loopingcall.FixedIntervalLoopingCall(_wait_unlock, 292 | expiration) 293 | timer.start(interval=interval).wait() 294 | 295 | def delete_userid(self, zhcp_node): 296 | """Delete z/VM userid for the instance.This will remove xCAT node 297 | at same time. 298 | """ 299 | url = self._xcat_url.rmvm('/' + self._name) 300 | 301 | try: 302 | zvmutils.xcat_request("DELETE", url) 303 | except exception.ZVMXCATInternalError as err: 304 | if (err.format_message().__contains__("Return Code: 400") and 305 | err.format_message().__contains__("Reason Code: 4")): 306 | # zVM user definition not found, delete xCAT node directly 307 | self.delete_xcat_node() 308 | elif (err.format_message().__contains__("Return Code: 400") and 309 | (err.format_message().__contains__("Reason Code: 16") or 310 | err.format_message().__contains__("Reason Code: 12"))): 311 | # The vm or vm device was locked. Unlock before deleting 312 | self._wait_for_unlock(zhcp_node) 313 | zvmutils.xcat_request("DELETE", url) 314 | else: 315 | raise err 316 | except exception.ZVMXCATRequestFailed as err: 317 | emsg = err.format_message() 318 | if (emsg.__contains__("Invalid nodes and/or groups") and 319 | emsg.__contains__("Forbidden")): 320 | # Assume neither zVM userid nor xCAT node exist in this case 321 | return 322 | else: 323 | raise err 324 | 325 | def delete_xcat_node(self): 326 | """Remove xCAT node for z/VM instance.""" 327 | url = self._xcat_url.rmdef('/' + self._name) 328 | try: 329 | zvmutils.xcat_request("DELETE", url) 330 | except exception.ZVMXCATInternalError as err: 331 | if err.format_message().__contains__("Could not find an object"): 332 | # The xCAT node not exist 333 | return 334 | else: 335 | raise err 336 | 337 | def add_mdisk(self, diskpool, vdev, size, fmt=None): 338 | """Add a 3390 mdisk for a z/VM user. 339 | 340 | NOTE: No read, write and multi password specified, and 341 | access mode default as 'MR'. 342 | 343 | """ 344 | disk_type = CONF.zvm_diskpool_type 345 | if (disk_type == 'ECKD'): 346 | action = '--add3390' 347 | elif (disk_type == 'FBA'): 348 | action = '--add9336' 349 | else: 350 | errmsg = _("Disk type %s is not supported.") % disk_type 351 | LOG.error(errmsg) 352 | raise exception.ZVMDriverError(msg=errmsg) 353 | 354 | if fmt: 355 | body = [" ".join([action, diskpool, vdev, size, "MR", "''", "''", 356 | "''", fmt])] 357 | else: 358 | body = [" ".join([action, diskpool, vdev, size])] 359 | url = self._xcat_url.chvm('/' + self._name) 360 | zvmutils.xcat_request("PUT", url, body) 361 | 362 | def _power_state(self, method, state): 363 | """Invoke xCAT REST API to set/get power state for a instance.""" 364 | body = [state] 365 | url = self._xcat_url.rpower('/' + self._name) 366 | return zvmutils.xcat_request(method, url, body) 367 | 368 | def _get_power_stat(self): 369 | """Get power status of a z/VM instance.""" 370 | LOG.debug(_('Query power stat of %s') % self._name) 371 | res_dict = self._power_state("GET", "stat") 372 | 373 | @zvmutils.wrap_invalid_xcat_resp_data_error 374 | def _get_power_string(d): 375 | tempstr = d['info'][0][0] 376 | return tempstr[(tempstr.find(':') + 2):].strip() 377 | 378 | power_stat = _get_power_string(res_dict) 379 | return zvmutils.mapping_power_stat(power_stat) 380 | 381 | def _get_rinv_info(self): 382 | """get rinv result and return in a list.""" 383 | url = self._xcat_url.rinv('/' + self._name, '&field=cpumem') 384 | LOG.debug(_('Remote inventory of %s') % self._name) 385 | res_info = zvmutils.xcat_request("GET", url)['info'] 386 | 387 | with zvmutils.expect_invalid_xcat_resp_data(): 388 | rinv_info = res_info[0][0].split('\n') 389 | 390 | return rinv_info 391 | 392 | @zvmutils.wrap_invalid_xcat_resp_data_error 393 | def _modify_storage_format(self, mem): 394 | """modify storage from 'G' ' M' to 'K'.""" 395 | new_mem = 0 396 | if mem.endswith('G'): 397 | new_mem = int(mem[:-1]) * 1024 * 1024 398 | elif mem.endswith('M'): 399 | new_mem = int(mem[:-1]) * 1024 400 | elif mem.endswith('K'): 401 | new_mem = int(mem[:-1]) 402 | else: 403 | exp = "ending with a 'G', 'M' or 'K'" 404 | errmsg = _("Invalid memory format: %(invalid)s; Expected: " 405 | "%(exp)s") % {'invalid': mem, 'exp': exp} 406 | LOG.error(errmsg) 407 | raise exception.ZVMInvalidXCATResponseDataError(msg=errmsg) 408 | return new_mem 409 | 410 | @zvmutils.wrap_invalid_xcat_resp_data_error 411 | def _get_current_memory(self, rec_list): 412 | """Return the max memory can be used.""" 413 | _mem = None 414 | 415 | for rec in rec_list: 416 | if rec.__contains__("Total Memory: "): 417 | tmp_list = rec.split() 418 | _mem = tmp_list[3] 419 | 420 | _mem = self._modify_storage_format(_mem) 421 | return _mem 422 | 423 | @zvmutils.wrap_invalid_xcat_resp_data_error 424 | def _get_cpu_count(self, rec_list): 425 | """Return the virtual cpu count.""" 426 | _cpu_flag = False 427 | num_cpu = 0 428 | 429 | for rec in rec_list: 430 | if (_cpu_flag is True): 431 | tmp_list = rec.split() 432 | if (len(tmp_list) > 1): 433 | if (tmp_list[1] == "CPU"): 434 | num_cpu += 1 435 | else: 436 | _cpu_flag = False 437 | if rec.__contains__("Processors: "): 438 | _cpu_flag = True 439 | 440 | return num_cpu 441 | 442 | @zvmutils.wrap_invalid_xcat_resp_data_error 443 | def _get_cpu_used_time(self, rec_list): 444 | """Return the cpu used time in.""" 445 | cpu_time = None 446 | 447 | for rec in rec_list: 448 | if rec.__contains__("CPU Used Time: "): 449 | tmp_list = rec.split() 450 | cpu_time = tmp_list[4] 451 | 452 | return int(cpu_time) 453 | 454 | def is_reachable(self): 455 | """Return True is the instance is reachable.""" 456 | url = self._xcat_url.nodestat('/' + self._name) 457 | LOG.debug(_('Get instance status of %s') % self._name) 458 | res_dict = zvmutils.xcat_request("GET", url) 459 | 460 | with zvmutils.expect_invalid_xcat_resp_data(): 461 | status = res_dict['node'][0][0]['data'][0] 462 | 463 | if status is not None: 464 | if status.__contains__('sshd'): 465 | return True 466 | 467 | return False 468 | 469 | def _wait_for_reachable(self): 470 | """Called at an interval until the instance is reachable.""" 471 | self._reachable = False 472 | 473 | def _wait_reachable(expiration): 474 | if (CONF.zvm_reachable_timeout and 475 | timeutils.utcnow() > expiration): 476 | raise loopingcall.LoopingCallDone() 477 | 478 | if self.is_reachable(): 479 | self._reachable = True 480 | LOG.debug(_("Instance %s reachable now") % 481 | self._name) 482 | raise loopingcall.LoopingCallDone() 483 | 484 | expiration = timeutils.utcnow() + datetime.timedelta( 485 | seconds=CONF.zvm_reachable_timeout) 486 | 487 | timer = loopingcall.FixedIntervalLoopingCall(_wait_reachable, 488 | expiration) 489 | timer.start(interval=5).wait() 490 | 491 | def update_node_info(self, image_meta): 492 | LOG.debug(_("Update the node info for instance %s") % self._name) 493 | 494 | image_name = image_meta['name'] 495 | image_id = image_meta['id'] 496 | os_type = image_meta['properties']['os_version'] 497 | os_arch = image_meta['properties']['architecture'] 498 | prov_method = image_meta['properties']['provisioning_method'] 499 | profile_name = '_'.join((image_name, image_id.replace('-', '_'))) 500 | 501 | body = ['noderes.netboot=%s' % const.HYPERVISOR_TYPE, 502 | 'nodetype.os=%s' % os_type, 503 | 'nodetype.arch=%s' % os_arch, 504 | 'nodetype.provmethod=%s' % prov_method, 505 | 'nodetype.profile=%s' % profile_name] 506 | url = self._xcat_url.chtab('/' + self._name) 507 | 508 | with zvmutils.except_xcat_call_failed_and_reraise( 509 | exception.ZVMXCATUpdateNodeFailed, node=self._name): 510 | zvmutils.xcat_request("PUT", url, body) 511 | 512 | def update_node_info_resize(self, image_name_xcat): 513 | LOG.debug(_("Update the nodetype for instance %s") % self._name) 514 | 515 | name_section = image_name_xcat.split("-") 516 | os_type = name_section[0] 517 | os_arch = name_section[1] 518 | profile_name = name_section[3] 519 | 520 | body = ['noderes.netboot=%s' % const.HYPERVISOR_TYPE, 521 | 'nodetype.os=%s' % os_type, 522 | 'nodetype.arch=%s' % os_arch, 523 | 'nodetype.provmethod=%s' % 'sysclone', 524 | 'nodetype.profile=%s' % profile_name] 525 | 526 | url = self._xcat_url.chtab('/' + self._name) 527 | 528 | with zvmutils.except_xcat_call_failed_and_reraise( 529 | exception.ZVMXCATUpdateNodeFailed, node=self._name): 530 | zvmutils.xcat_request("PUT", url, body) 531 | 532 | def get_provmethod(self): 533 | addp = "&col=node=%s&attribute=provmethod" % self._name 534 | url = self._xcat_url.gettab('/nodetype', addp) 535 | res_info = zvmutils.xcat_request("GET", url) 536 | return res_info['data'][0][0] 537 | 538 | def update_node_provmethod(self, provmethod): 539 | LOG.debug(_("Update the nodetype for instance %s") % self._name) 540 | 541 | body = ['nodetype.provmethod=%s' % provmethod] 542 | 543 | url = self._xcat_url.chtab('/' + self._name) 544 | 545 | with zvmutils.except_xcat_call_failed_and_reraise( 546 | exception.ZVMXCATUpdateNodeFailed, node=self._name): 547 | zvmutils.xcat_request("PUT", url, body) 548 | 549 | def update_node_def(self, hcp, userid): 550 | """Update xCAT node definition.""" 551 | 552 | body = ['zvm.hcp=%s' % hcp, 553 | 'zvm.userid=%s' % userid] 554 | url = self._xcat_url.chtab('/' + self._name) 555 | 556 | with zvmutils.except_xcat_call_failed_and_reraise( 557 | exception.ZVMXCATUpdateNodeFailed, node=self._name): 558 | zvmutils.xcat_request("PUT", url, body) 559 | 560 | def deploy_node(self, image_name, transportfiles=None, vdev=None): 561 | LOG.debug(_("Begin to deploy image on instance %s") % self._name) 562 | vdev = vdev or CONF.zvm_user_root_vdev 563 | remote_host_info = zvmutils.get_host() 564 | body = ['netboot', 565 | 'device=%s' % vdev, 566 | 'osimage=%s' % image_name] 567 | 568 | if transportfiles: 569 | body.append('transport=%s' % transportfiles) 570 | body.append('remotehost=%s' % remote_host_info) 571 | 572 | url = self._xcat_url.nodeset('/' + self._name) 573 | 574 | with zvmutils.except_xcat_call_failed_and_reraise( 575 | exception.ZVMXCATDeployNodeFailed, node=self._name): 576 | zvmutils.xcat_request("PUT", url, body) 577 | 578 | def copy_xcat_node(self, source_node_name): 579 | """Create xCAT node from an existing z/VM instance.""" 580 | LOG.debug(_("Creating xCAT node %s from existing node") % self._name) 581 | 582 | url = self._xcat_url.lsdef_node('/' + source_node_name) 583 | res_info = zvmutils.xcat_request("GET", url)['info'][0] 584 | 585 | body = [] 586 | for info in res_info: 587 | if "=" in info and ("postbootscripts" not in info)\ 588 | and ("postscripts" not in info) \ 589 | and ("hostnames" not in info): 590 | body.append(info.lstrip()) 591 | 592 | url = self._xcat_url.mkdef('/' + self._name) 593 | 594 | with zvmutils.except_xcat_call_failed_and_reraise( 595 | exception.ZVMXCATCreateNodeFailed, node=self._name): 596 | zvmutils.xcat_request("POST", url, body) 597 | 598 | def get_console_log(self, logsize): 599 | """get console log.""" 600 | url = self._xcat_url.rinv('/' + self._name, '&field=console' 601 | '&field=%s') % logsize 602 | 603 | LOG.debug(_('Get console log of %s') % self._name) 604 | res_info = zvmutils.xcat_request("GET", url)['info'] 605 | 606 | with zvmutils.expect_invalid_xcat_resp_data(): 607 | rinv_info = res_info[0][0] 608 | 609 | return rinv_info 610 | -------------------------------------------------------------------------------- /nova-zvm-virt-driver/nova/virt/zvm/networkop.py: -------------------------------------------------------------------------------- 1 | # vim: tabstop=4 shiftwidth=4 softtabstop=4 2 | 3 | # Copyright 2013 IBM Corp. 4 | # 5 | # Licensed under the Apache License, Version 2.0 (the "License"); you may 6 | # not use this file except in compliance with the License. You may obtain 7 | # a copy of the License at 8 | # 9 | # http://www.apache.org/licenses/LICENSE-2.0 10 | # 11 | # Unless required by applicable law or agreed to in writing, software 12 | # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 13 | # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 14 | # License for the specific language governing permissions and limitations 15 | # under the License. 16 | 17 | 18 | from oslo.config import cfg 19 | 20 | from nova.openstack.common.gettextutils import _ 21 | from nova.openstack.common import importutils 22 | from nova.openstack.common import log as logging 23 | from nova.virt.zvm import exception 24 | from nova.virt.zvm import utils as zvmutils 25 | 26 | 27 | LOG = logging.getLogger(__name__) 28 | 29 | CONF = cfg.CONF 30 | 31 | NetworkUtils = zvmutils.NetworkUtils() 32 | 33 | 34 | class NetworkOperator(object): 35 | """Configuration check and manage MAC address.""" 36 | 37 | _vif_driver_class_map = { 38 | 'nova.network.neutronv2.api.API': 39 | 'nova.virt.zvm.vif.ZVMNeutronVIFDriver', 40 | } 41 | 42 | def __init__(self): 43 | self._xcat_url = zvmutils.XCATUrl() 44 | self._load_vif_driver_class() 45 | 46 | def _load_vif_driver_class(self): 47 | vif_driver = CONF.network_api_class 48 | try: 49 | class_name = self._vif_driver_class_map[vif_driver] 50 | except KeyError: 51 | msg = _("VIF driver %s not supported by z/VM.") % vif_driver 52 | LOG.error(msg) 53 | raise exception.ZVMNetworkError(msg=msg) 54 | 55 | self._vif_driver = importutils.import_object(class_name) 56 | 57 | def add_xcat_host(self, node, ip, host_name): 58 | """Add/Update hostname/ip bundle in xCAT MN nodes table.""" 59 | commands = "node=%s" % node + " hosts.ip=%s" % ip 60 | commands += " hosts.hostnames=%s" % host_name 61 | body = [commands] 62 | url = self._xcat_url.tabch("/hosts") 63 | 64 | with zvmutils.except_xcat_call_failed_and_reraise( 65 | exception.ZVMNetworkError): 66 | result_data = zvmutils.xcat_request("PUT", url, body)['data'] 67 | 68 | return result_data 69 | 70 | def _delete_xcat_host(self, node_name): 71 | """Remove xcat hosts table rows where node name is node_name.""" 72 | commands = "-d node=%s hosts" % node_name 73 | body = [commands] 74 | url = self._xcat_url.tabch("/hosts") 75 | 76 | with zvmutils.except_xcat_call_failed_and_reraise( 77 | exception.ZVMNetworkError): 78 | return zvmutils.xcat_request("PUT", url, body)['data'] 79 | 80 | def add_xcat_mac(self, node, interface, mac, zhcp=None): 81 | """Add node name, interface, mac address into xcat mac table.""" 82 | commands = "mac.node=%s" % node + " mac.mac=%s" % mac 83 | commands += " mac.interface=%s" % interface 84 | if zhcp is not None: 85 | commands += " mac.comments=%s" % zhcp 86 | url = self._xcat_url.tabch("/mac") 87 | body = [commands] 88 | 89 | with zvmutils.except_xcat_call_failed_and_reraise( 90 | exception.ZVMNetworkError): 91 | return zvmutils.xcat_request("PUT", url, body)['data'] 92 | 93 | def add_xcat_switch(self, node, nic_name, interface, zhcp=None): 94 | """Add node name and nic name address into xcat switch table.""" 95 | commands = "switch.node=%s" % node 96 | commands += " switch.port=%s" % nic_name 97 | commands += " switch.interface=%s" % interface 98 | if zhcp is not None: 99 | commands += " switch.comments=%s" % zhcp 100 | url = self._xcat_url.tabch("/switch") 101 | body = [commands] 102 | 103 | with zvmutils.except_xcat_call_failed_and_reraise( 104 | exception.ZVMNetworkError): 105 | return zvmutils.xcat_request("PUT", url, body)['data'] 106 | 107 | def _delete_xcat_mac(self, node_name): 108 | """Remove node mac record from xcat mac table.""" 109 | commands = "-d node=%s mac" % node_name 110 | url = self._xcat_url.tabch("/mac") 111 | body = [commands] 112 | 113 | with zvmutils.except_xcat_call_failed_and_reraise( 114 | exception.ZVMNetworkError): 115 | return zvmutils.xcat_request("PUT", url, body)['data'] 116 | 117 | def _delete_xcat_switch(self, node_name): 118 | """Remove node switch record from xcat switch table.""" 119 | commands = "-d node=%s switch" % node_name 120 | url = self._xcat_url.tabch("/switch") 121 | body = [commands] 122 | 123 | with zvmutils.except_xcat_call_failed_and_reraise( 124 | exception.ZVMNetworkError): 125 | return zvmutils.xcat_request("PUT", url, body)['data'] 126 | 127 | def update_xcat_mac(self, node, interface, mac, zhcp=None): 128 | """Add node name, interface, mac address into xcat mac table.""" 129 | commands = "node=%s" % node + " interface=%s" % interface 130 | commands += " mac.mac=%s" % mac 131 | if zhcp is not None: 132 | commands += " mac.comments=%s" % zhcp 133 | url = self._xcat_url.tabch("/mac") 134 | body = [commands] 135 | 136 | with zvmutils.except_xcat_call_failed_and_reraise( 137 | exception.ZVMNetworkError): 138 | return zvmutils.xcat_request("PUT", url, body)['data'] 139 | 140 | def update_xcat_switch(self, node, nic_name, interface, zhcp=None): 141 | """Add node name and nic name address into xcat switch table.""" 142 | commands = "node=%s" % node 143 | commands += " interface=%s" % interface 144 | commands += " switch.port=%s" % nic_name 145 | if zhcp is not None: 146 | commands += " switch.comments=%s" % zhcp 147 | url = self._xcat_url.tabch("/switch") 148 | body = [commands] 149 | 150 | with zvmutils.except_xcat_call_failed_and_reraise( 151 | exception.ZVMNetworkError): 152 | return zvmutils.xcat_request("PUT", url, body)['data'] 153 | 154 | def clean_mac_switch_host(self, node_name): 155 | """Clean node records in xCAT mac, host and switch table.""" 156 | self.clean_mac_switch(node_name) 157 | self._delete_xcat_host(node_name) 158 | 159 | def clean_mac_switch(self, node_name): 160 | """Clean node records in xCAT mac and switch table.""" 161 | self._delete_xcat_mac(node_name) 162 | self._delete_xcat_switch(node_name) 163 | 164 | def makehosts(self): 165 | """Update xCAT MN /etc/hosts file.""" 166 | url = self._xcat_url.network("/makehosts") 167 | with zvmutils.except_xcat_call_failed_and_reraise( 168 | exception.ZVMNetworkError): 169 | return zvmutils.xcat_request("PUT", url)['data'] 170 | 171 | def makeDNS(self): 172 | """Update xCAT MN DNS.""" 173 | url = self._xcat_url.network("/makedns") 174 | with zvmutils.except_xcat_call_failed_and_reraise( 175 | exception.ZVMNetworkError): 176 | return zvmutils.xcat_request("PUT", url)['data'] 177 | 178 | def config_xcat_mac(self, instance_name): 179 | """Hook xCat to prevent assign MAC for instance.""" 180 | fake_mac_addr = "00:00:00:00:00:00" 181 | nic_name = "fake" 182 | self.add_xcat_mac(instance_name, nic_name, fake_mac_addr) 183 | 184 | def create_nic(self, zhcpnode, inst_name, nic_name, mac_address, vdev, 185 | userid=None): 186 | """Create network information in xCAT and zVM user direct.""" 187 | macid = mac_address.replace(':', '')[-6:] 188 | self._add_instance_nic(zhcpnode, inst_name, vdev, macid, userid) 189 | self._delete_xcat_mac(inst_name) 190 | self.add_xcat_mac(inst_name, vdev, mac_address, zhcpnode) 191 | self.add_xcat_switch(inst_name, nic_name, vdev, zhcpnode) 192 | 193 | def _add_instance_nic(self, zhcpnode, inst_name, vdev, macid, userid=None): 194 | """Add NIC defination into user direct.""" 195 | if userid is None: 196 | command = ("/opt/zhcp/bin/smcli Image_Definition_Update_DM -T %s" % 197 | inst_name) 198 | else: 199 | command = ("/opt/zhcp/bin/smcli Image_Definition_Update_DM -T %s" % 200 | userid) 201 | command += " -k \'NICDEF=VDEV=%s TYPE=QDIO " % vdev 202 | command += "MACID=%s\'" % macid 203 | try: 204 | zvmutils.xdsh(zhcpnode, command) 205 | except exception.ZVMXCATXdshFailed as err: 206 | msg = _("Adding nic error: %s") % err 207 | raise exception.ZVMNetworkError(msg=msg) 208 | -------------------------------------------------------------------------------- /nova-zvm-virt-driver/nova/virt/zvm/utils.py: -------------------------------------------------------------------------------- 1 | # vim: tabstop=4 shiftwidth=4 softtabstop=4 2 | 3 | # Copyright 2013 IBM Corp. 4 | # 5 | # Licensed under the Apache License, Version 2.0 (the "License"); you may 6 | # not use this file except in compliance with the License. You may obtain 7 | # a copy of the License at 8 | # 9 | # http://www.apache.org/licenses/LICENSE-2.0 10 | # 11 | # Unless required by applicable law or agreed to in writing, software 12 | # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 13 | # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 14 | # License for the specific language governing permissions and limitations 15 | # under the License. 16 | 17 | 18 | import contextlib 19 | import functools 20 | import httplib 21 | import os 22 | import shutil 23 | import socket 24 | import time 25 | 26 | from oslo.config import cfg 27 | 28 | from nova import block_device 29 | from nova.compute import power_state 30 | from nova.openstack.common import excutils 31 | from nova.openstack.common.gettextutils import _ 32 | from nova.openstack.common import jsonutils 33 | from nova.openstack.common import log as logging 34 | from nova.virt import driver 35 | from nova.virt.zvm import const 36 | from nova.virt.zvm import exception 37 | 38 | 39 | LOG = logging.getLogger(__name__) 40 | 41 | CONF = cfg.CONF 42 | CONF.import_opt('instances_path', 'nova.compute.manager') 43 | 44 | 45 | class XCATUrl(object): 46 | """To return xCAT url for invoking xCAT REST API.""" 47 | 48 | def __init__(self): 49 | """Set constant that used to form xCAT url.""" 50 | self.PREFIX = '/xcatws' 51 | self.SUFFIX = '?userName=' + CONF.zvm_xcat_username + \ 52 | '&password=' + CONF.zvm_xcat_password + \ 53 | '&format=json' 54 | 55 | self.NODES = '/nodes' 56 | self.VMS = '/vms' 57 | self.IMAGES = '/images' 58 | self.OBJECTS = '/objects/osimage' 59 | self.OS = '/OS' 60 | self.TABLES = '/tables' 61 | self.HV = '/hypervisor' 62 | self.NETWORK = '/networks' 63 | 64 | self.POWER = '/power' 65 | self.INVENTORY = '/inventory' 66 | self.STATUS = '/status' 67 | self.MIGRATE = '/migrate' 68 | self.CAPTURE = '/capture' 69 | self.EXPORT = '/export' 70 | self.IMGIMPORT = '/import' 71 | self.BOOTSTAT = '/bootstate' 72 | self.XDSH = '/dsh' 73 | 74 | def _nodes(self, arg=''): 75 | return self.PREFIX + self.NODES + arg + self.SUFFIX 76 | 77 | def _vms(self, arg=''): 78 | return self.PREFIX + self.VMS + arg + self.SUFFIX 79 | 80 | def _hv(self, arg=''): 81 | return self.PREFIX + self.HV + arg + self.SUFFIX 82 | 83 | def rpower(self, arg=''): 84 | return self.PREFIX + self.NODES + arg + self.POWER + self.SUFFIX 85 | 86 | def nodels(self, arg=''): 87 | return self._nodes(arg) 88 | 89 | def rinv(self, arg='', addp=None): 90 | rurl = self.PREFIX + self.NODES + arg + self.INVENTORY + self.SUFFIX 91 | return self._append_addp(rurl, addp) 92 | 93 | def mkdef(self, arg=''): 94 | return self._nodes(arg) 95 | 96 | def rmdef(self, arg=''): 97 | return self._nodes(arg) 98 | 99 | def nodestat(self, arg=''): 100 | return self.PREFIX + self.NODES + arg + self.STATUS + self.SUFFIX 101 | 102 | def chvm(self, arg=''): 103 | return self._vms(arg) 104 | 105 | def lsvm(self, arg=''): 106 | return self._vms(arg) 107 | 108 | def chhv(self, arg=''): 109 | return self._hv(arg) 110 | 111 | def mkvm(self, arg=''): 112 | return self._vms(arg) 113 | 114 | def rmvm(self, arg=''): 115 | return self._vms(arg) 116 | 117 | def tabdump(self, arg='', addp=None): 118 | rurl = self.PREFIX + self.TABLES + arg + self.SUFFIX 119 | return self._append_addp(rurl, addp) 120 | 121 | def _append_addp(self, rurl, addp=None): 122 | if addp is not None: 123 | return rurl + addp 124 | else: 125 | return rurl 126 | 127 | def imgcapture(self, arg=''): 128 | return self.PREFIX + self.IMAGES + arg + self.CAPTURE + self.SUFFIX 129 | 130 | def imgexport(self, arg=''): 131 | return self.PREFIX + self.IMAGES + arg + self.EXPORT + self.SUFFIX 132 | 133 | def rmimage(self, arg=''): 134 | return self.PREFIX + self.IMAGES + arg + self.SUFFIX 135 | 136 | def rmobject(self, arg=''): 137 | return self.PREFIX + self.OBJECTS + arg + self.SUFFIX 138 | 139 | def lsdef_node(self, arg='', addp=None): 140 | rurl = self.PREFIX + self.NODES + arg + self.SUFFIX 141 | return self._append_addp(rurl, addp) 142 | 143 | def lsdef_image(self, arg='', addp=None): 144 | rurl = self.PREFIX + self.IMAGES + arg + self.SUFFIX 145 | return self._append_addp(rurl, addp) 146 | 147 | def imgimport(self, arg=''): 148 | return self.PREFIX + self.IMAGES + self.IMGIMPORT + arg + self.SUFFIX 149 | 150 | def chtab(self, arg=''): 151 | return self.PREFIX + self.NODES + arg + self.SUFFIX 152 | 153 | def nodeset(self, arg=''): 154 | return self.PREFIX + self.NODES + arg + self.BOOTSTAT + self.SUFFIX 155 | 156 | def rmigrate(self, arg=''): 157 | return self.PREFIX + self.NODES + arg + self.MIGRATE + self.SUFFIX 158 | 159 | def gettab(self, arg='', addp=None): 160 | rurl = self.PREFIX + self.TABLES + arg + self.SUFFIX 161 | return self._append_addp(rurl, addp) 162 | 163 | def tabch(self, arg='', addp=None): 164 | """Add/update/delete row(s) in table arg, with attribute addp.""" 165 | rurl = self.PREFIX + self.TABLES + arg + self.SUFFIX 166 | return self._append_addp(rurl, addp) 167 | 168 | def xdsh(self, arg=''): 169 | """Run shell command.""" 170 | return self.PREFIX + self.NODES + arg + self.XDSH + self.SUFFIX 171 | 172 | def network(self, arg='', addp=None): 173 | rurl = self.PREFIX + self.NETWORK + arg + self.SUFFIX 174 | if addp is not None: 175 | return rurl + addp 176 | else: 177 | return rurl 178 | 179 | 180 | class XCATConnection(): 181 | """Https requests to xCAT web service.""" 182 | 183 | def __init__(self): 184 | """Initialize https connection to xCAT service.""" 185 | self.host = CONF.zvm_xcat_server 186 | self.conn = httplib.HTTPSConnection(self.host, 187 | timeout=CONF.zvm_xcat_connection_timeout) 188 | 189 | def request(self, method, url, body=None, headers={}): 190 | """Send https request to xCAT server. 191 | 192 | Will return a python dictionary including: 193 | {'status': http return code, 194 | 'reason': http reason, 195 | 'message': response message} 196 | 197 | """ 198 | if body is not None: 199 | body = jsonutils.dumps(body) 200 | headers = {'content-type': 'text/plain', 201 | 'content-length': len(body)} 202 | 203 | try: 204 | self.conn.request(method, url, body, headers) 205 | except socket.gaierror as err: 206 | msg = _("Failed to find address: %s") % err 207 | raise exception.ZVMXCATRequestFailed(xcatserver=self.host, msg=msg) 208 | except (socket.error, socket.timeout) as err: 209 | msg = _("Communication error: %s") % err 210 | raise exception.ZVMXCATRequestFailed(xcatserver=self.host, msg=msg) 211 | 212 | try: 213 | res = self.conn.getresponse() 214 | except Exception as err: 215 | msg = _("Failed to get response from xCAT: %s") % err 216 | raise exception.ZVMXCATRequestFailed(xcatserver=self.host, msg=msg) 217 | 218 | msg = res.read() 219 | resp = { 220 | 'status': res.status, 221 | 'reason': res.reason, 222 | 'message': msg} 223 | 224 | # Only "200" or "201" returned from xCAT can be considered 225 | # as good status 226 | err = None 227 | if method == "POST": 228 | if res.status != 201: 229 | err = str(resp) 230 | else: 231 | if res.status != 200: 232 | err = str(resp) 233 | 234 | if err is not None: 235 | raise exception.ZVMXCATRequestFailed(xcatserver=self.host, 236 | msg=err) 237 | 238 | return resp 239 | 240 | 241 | def xcat_request(method, url, body=None, headers={}): 242 | conn = XCATConnection() 243 | resp = conn.request(method, url, body, headers) 244 | return load_xcat_resp(resp['message']) 245 | 246 | 247 | def jsonloads(jsonstr): 248 | try: 249 | return jsonutils.loads(jsonstr) 250 | except ValueError: 251 | errmsg = _("xCAT response data is not in JSON format") 252 | LOG.error(errmsg) 253 | raise exception.ZVMDriverError(msg=errmsg) 254 | 255 | 256 | @contextlib.contextmanager 257 | def expect_invalid_xcat_resp_data(): 258 | """Catch exceptions when using xCAT response data.""" 259 | try: 260 | yield 261 | except (ValueError, TypeError, IndexError, AttributeError, 262 | KeyError) as err: 263 | raise exception.ZVMInvalidXCATResponseDataError(msg=err) 264 | 265 | 266 | def wrap_invalid_xcat_resp_data_error(function): 267 | """Catch exceptions when using xCAT response data.""" 268 | 269 | @functools.wraps(function) 270 | def decorated_function(*arg, **kwargs): 271 | try: 272 | return function(*arg, **kwargs) 273 | except (ValueError, TypeError, IndexError, AttributeError, 274 | KeyError) as err: 275 | raise exception.ZVMInvalidXCATResponseDataError(msg=err) 276 | 277 | return decorated_function 278 | 279 | 280 | @contextlib.contextmanager 281 | def ignore_errors(): 282 | """Only execute the clauses and ignore the results.""" 283 | 284 | try: 285 | yield 286 | except Exception as err: 287 | LOG.debug(_("Ignore an error: %s") % err) 288 | pass 289 | 290 | 291 | @contextlib.contextmanager 292 | def except_xcat_call_failed_and_reraise(exc, **kwargs): 293 | """Catch all kinds of xCAT call failure and reraise. 294 | 295 | exc: the exception that would be raised. 296 | """ 297 | try: 298 | yield 299 | except (exception.ZVMXCATRequestFailed, 300 | exception.ZVMInvalidXCATResponseDataError, 301 | exception.ZVMXCATInternalError) as err: 302 | kwargs['msg'] = err 303 | raise exc(**kwargs) 304 | 305 | 306 | def convert_to_mb(s): 307 | """Convert memory size from GB to MB.""" 308 | s = s.upper() 309 | try: 310 | if s.endswith('G'): 311 | return float(s[:-1].strip()) * 1024 312 | else: 313 | return float(s[:-1].strip()) 314 | except (IndexError, ValueError, KeyError, TypeError) as e: 315 | errmsg = _("Invalid memory format: %s") % e 316 | raise exception.ZVMDriverError(msg=errmsg) 317 | 318 | 319 | @wrap_invalid_xcat_resp_data_error 320 | def translate_xcat_resp(rawdata, dirt): 321 | """Translate xCAT response JSON stream to a python dictionary. 322 | 323 | xCAT response example: 324 | node: keyword1: value1\n 325 | node: keyword2: value2\n 326 | ... 327 | node: keywordn: valuen\n 328 | 329 | Will return a python dictionary: 330 | {keyword1: value1, 331 | keyword2: value2, 332 | ... 333 | keywordn: valuen,} 334 | 335 | """ 336 | data_list = rawdata.split("\n") 337 | 338 | data = {} 339 | 340 | for ls in data_list: 341 | for k in dirt.keys(): 342 | if ls.__contains__(dirt[k]): 343 | data[k] = ls[(ls.find(dirt[k]) + len(dirt[k])):].strip() 344 | break 345 | 346 | if data == {}: 347 | msg = _("No value matched with keywords. Raw Data: %(raw)s; " 348 | "Keywords: %(kws)s") % {'raw': rawdata, 'kws': str(dirt)} 349 | raise exception.ZVMInvalidXCATResponseDataError(msg=msg) 350 | 351 | return data 352 | 353 | 354 | def mapping_power_stat(power_stat): 355 | """Translate power state to OpenStack defined constants.""" 356 | return const.ZVM_POWER_STAT.get(power_stat, power_state.NOSTATE) 357 | 358 | 359 | @wrap_invalid_xcat_resp_data_error 360 | def load_xcat_resp(message): 361 | """Abstract information from xCAT REST response body. 362 | 363 | As default, xCAT response will in format of JSON and can be 364 | converted to Python dictionary, would looks like: 365 | {"data": [{"info": [info,]}, {"data": [data,]}, ..., {"error": [error,]}]} 366 | 367 | Returns a Python dictionary, looks like: 368 | {'info': [info,], 369 | 'data': [data,], 370 | ... 371 | 'error': [error,]} 372 | 373 | """ 374 | resp_list = jsonloads(message)['data'] 375 | keys = const.XCAT_RESPONSE_KEYS 376 | 377 | resp = {} 378 | 379 | for k in keys: 380 | resp[k] = [] 381 | 382 | for d in resp_list: 383 | for k in keys: 384 | if d.get(k) is not None: 385 | resp[k].append(d.get(k)) 386 | 387 | err = resp.get('error') 388 | if err != []: 389 | for e in err: 390 | if _is_warning(str(e)): 391 | # ignore known warnings 392 | continue 393 | else: 394 | raise exception.ZVMXCATInternalError(msg=message) 395 | 396 | _log_warnings(resp) 397 | 398 | return resp 399 | 400 | 401 | def _log_warnings(resp): 402 | for msg in (resp['info'], resp['node'], resp['data']): 403 | msgstr = str(msg) 404 | if 'warn' in msgstr.lower(): 405 | LOG.warn(_("Warning from xCAT: %s") % msgstr) 406 | 407 | 408 | def _is_warning(err_str): 409 | ignore_list = ( 410 | 'Warning: the RSA host key for', 411 | 'Warning: Permanently added', 412 | 'WARNING: REMOTE HOST IDENTIFICATION HAS CHANGED', 413 | ) 414 | 415 | for im in ignore_list: 416 | if im in err_str: 417 | return True 418 | 419 | return False 420 | 421 | 422 | def volume_in_mapping(mount_device, block_device_info): 423 | block_device_list = [block_device.strip_dev(vol['mount_device']) 424 | for vol in 425 | driver.block_device_info_get_mapping( 426 | block_device_info)] 427 | swap = driver.block_device_info_get_swap(block_device_info) 428 | if driver.swap_is_usable(swap): 429 | block_device_list.append( 430 | block_device.strip_dev(swap['device_name'])) 431 | block_device_list += [block_device.strip_dev(ephemeral['device_name']) 432 | for ephemeral in 433 | driver.block_device_info_get_ephemerals( 434 | block_device_info)] 435 | 436 | LOG.debug(_("block_device_list %s"), block_device_list) 437 | return block_device.strip_dev(mount_device) in block_device_list 438 | 439 | 440 | def get_host(): 441 | return ''.join([os.environ["USER"], '@', CONF.my_ip]) 442 | 443 | 444 | def get_userid(node_name): 445 | """Returns z/VM userid for the xCAT node.""" 446 | url = XCATUrl().lsdef_node(''.join(['/', node_name])) 447 | info = xcat_request('GET', url)['info'] 448 | 449 | with expect_invalid_xcat_resp_data(): 450 | for s in info[0]: 451 | if s.__contains__('userid='): 452 | return s.strip().rpartition('=')[2] 453 | 454 | 455 | def xdsh(node, commands): 456 | """"Run command on xCAT node.""" 457 | LOG.debug(_('Run command %(cmd)s on xCAT node %(node)s') % 458 | {'cmd': commands, 'node': node}) 459 | 460 | def xdsh_execute(node, commands): 461 | """Invoke xCAT REST API to execute command on node.""" 462 | xdsh_commands = 'command=%s' % commands 463 | body = [xdsh_commands] 464 | url = XCATUrl().xdsh('/' + node) 465 | return xcat_request("PUT", url, body) 466 | 467 | with except_xcat_call_failed_and_reraise( 468 | exception.ZVMXCATXdshFailed): 469 | res_dict = xdsh_execute(node, commands) 470 | 471 | return res_dict 472 | 473 | 474 | def punch_file(node, fn, fclass): 475 | body = [" ".join(['--punchfile', fn, fclass, get_host()])] 476 | url = XCATUrl().chvm('/' + node) 477 | 478 | try: 479 | xcat_request("PUT", url, body) 480 | except Exception as err: 481 | with excutils.save_and_reraise_exception(): 482 | LOG.error(_('Punch file to %(node)s failed: %(msg)s') % 483 | {'node': node, 'msg': err}) 484 | finally: 485 | os.remove(fn) 486 | 487 | 488 | def punch_adminpass_file(instance_path, instance_name, admin_password): 489 | adminpass_fn = ''.join([instance_path, '/adminpwd.sh']) 490 | _generate_adminpass_file(adminpass_fn, admin_password) 491 | punch_file(instance_name, adminpass_fn, 'X') 492 | 493 | 494 | def punch_xcat_auth_file(instance_path, instance_name): 495 | """Make xCAT MN authorized by virtual machines.""" 496 | mn_pub_key = get_mn_pub_key() 497 | auth_fn = ''.join([instance_path, '/xcatauth.sh']) 498 | _generate_auth_file(auth_fn, mn_pub_key) 499 | punch_file(instance_name, auth_fn, 'X') 500 | 501 | 502 | def punch_eph_info_file(instance_path, instance_name, vdev=None, fmt=None, 503 | mntdir=None): 504 | if not fmt: 505 | fmt = CONF.default_ephemeral_format or const.DEFAULT_EPH_DISK_FMT 506 | eph_fn = ''.join([instance_path, '/eph.disk']) 507 | _generate_ephinfo_file(eph_fn, vdev, fmt, mntdir) 508 | punch_file(instance_name, eph_fn, 'X') 509 | 510 | 511 | def generate_vdev(base, offset=1): 512 | """Generate virtual device number base on base vdev. 513 | 514 | :param base: base virtual device number, string of 4 bit hex. 515 | :param offset: offset to base, integer. 516 | 517 | :output: virtual device number, string of 4 bit hex. 518 | """ 519 | vdev = hex(int(base, 16) + offset)[2:] 520 | return vdev.rjust(4, '0') 521 | 522 | 523 | def generate_eph_vdev(offset=1): 524 | """Generate virtual device number for ephemeral disks. 525 | 526 | :parm offset: offset to zvm_user_adde_vdev. 527 | 528 | :output: virtual device number, string of 4 bit hex. 529 | """ 530 | vdev = generate_vdev(CONF.zvm_user_adde_vdev, offset + 1) 531 | if offset >= 0 and offset < 254: 532 | return vdev 533 | else: 534 | msg = _("Invalid virtual device number for ephemeral disk: %s") % vdev 535 | LOG.error(msg) 536 | raise exception.ZVMDriverError(msg=msg) 537 | 538 | 539 | def _generate_ephinfo_file(fname, vdev, fmt, mntdir): 540 | vdev = vdev or CONF.zvm_user_adde_vdev 541 | mntdir = mntdir or CONF.zvm_default_ephemeral_mntdir 542 | 543 | lines = [ 544 | '# xCAT Init\n', 545 | 'action=addMdisk\n', 546 | 'vaddr=' + vdev + '\n', 547 | 'filesys=' + fmt + '\n', 548 | 'mntdir=' + mntdir + '\n' 549 | ] 550 | 551 | with open(fname, 'w') as genfile: 552 | try: 553 | genfile.writelines(lines) 554 | except Exception as err: 555 | with excutils.save_and_reraise_exception(): 556 | LOG.error(_('Generate ephemeral info file failed: %s') % err) 557 | 558 | 559 | def _generate_auth_file(fn, pub_key): 560 | lines = ['#!/bin/bash\n', 561 | 'echo "%s" >> /root/.ssh/authorized_keys' % pub_key] 562 | with open(fn, 'w') as f: 563 | f.writelines(lines) 564 | 565 | 566 | def _generate_adminpass_file(fn, admin_password): 567 | lines = ['#! /bin/bash\n', 568 | 'echo %s|passwd --stdin root' % admin_password] 569 | with open(fn, 'w') as f: 570 | f.writelines(lines) 571 | 572 | 573 | @wrap_invalid_xcat_resp_data_error 574 | def get_mn_pub_key(): 575 | cmd = 'cat /root/.ssh/id_rsa.pub' 576 | resp = xdsh(CONF.zvm_xcat_master, cmd) 577 | key = resp['data'][0][0] 578 | start_idx = key.find('ssh-rsa') 579 | key = key[start_idx:] 580 | return key 581 | 582 | 583 | def parse_os_version(os_version): 584 | """Separate os and version from os_version. 585 | Possible return value are only: 586 | ('rhel', x.y) and ('sles', x.y) where x.y may not be digits 587 | """ 588 | supported = {'rhel': ['rhel', 'redhat', 'red hat'], 589 | 'sles': ['suse', 'sles']} 590 | os_version = os_version.lower() 591 | for distro, patterns in supported.items(): 592 | for i in patterns: 593 | if os_version.startswith(i): 594 | # Not guarrentee the version is digital 595 | return distro, os_version.split(i, 2)[1] 596 | else: 597 | raise exception.ZVMImageError(msg='Unknown os_version property') 598 | 599 | 600 | class PathUtils(object): 601 | def open(self, path, mode): 602 | """Wrapper on __builin__.open used to simplify unit testing.""" 603 | import __builtin__ 604 | return __builtin__.open(path, mode) 605 | 606 | def _get_image_tmp_path(self): 607 | image_tmp_path = os.path.normpath(CONF.zvm_image_tmp_path) 608 | if not os.path.exists(image_tmp_path): 609 | LOG.debug(_('Creating folder %s for image temp files') % 610 | image_tmp_path) 611 | os.makedirs(image_tmp_path) 612 | return image_tmp_path 613 | 614 | def get_bundle_tmp_path(self, tmp_file_fn): 615 | bundle_tmp_path = os.path.join(self._get_image_tmp_path(), "spawn_tmp", 616 | tmp_file_fn) 617 | if not os.path.exists(bundle_tmp_path): 618 | LOG.debug(_('Creating folder %s for image bundle temp file') % 619 | bundle_tmp_path) 620 | os.makedirs(bundle_tmp_path) 621 | return bundle_tmp_path 622 | 623 | def get_img_path(self, bundle_file_path, image_name): 624 | return os.path.join(bundle_file_path, image_name) 625 | 626 | def _get_snapshot_path(self): 627 | snapshot_folder = os.path.join(self._get_image_tmp_path(), 628 | "snapshot_tmp") 629 | if not os.path.exists(snapshot_folder): 630 | LOG.debug(_("Creating the snapshot folder %s") % snapshot_folder) 631 | os.makedirs(snapshot_folder) 632 | return snapshot_folder 633 | 634 | def _get_punch_path(self): 635 | punch_folder = os.path.join(self._get_image_tmp_path(), "punch_tmp") 636 | if not os.path.exists(punch_folder): 637 | LOG.debug(_("Creating the punch folder %s") % punch_folder) 638 | os.makedirs(punch_folder) 639 | return punch_folder 640 | 641 | def _make_short_time_stamp(self): 642 | # tmp_file_fn = time.strftime('%d%H%M%S', 643 | # time.localtime(time.time())) 644 | # return tmp_file_fn 645 | return '0' 646 | 647 | def get_punch_time_path(self): 648 | punch_time_path = os.path.join(self._get_punch_path(), 649 | self._make_short_time_stamp()) 650 | if not os.path.exists(punch_time_path): 651 | LOG.debug(_("Creating punch time folder %s") % punch_time_path) 652 | os.makedirs(punch_time_path) 653 | return punch_time_path 654 | 655 | def get_spawn_folder(self): 656 | spawn_folder = os.path.join(self._get_image_tmp_path(), "spawn_tmp") 657 | if not os.path.exists(spawn_folder): 658 | LOG.debug(_("Creating the spawn folder %s") % spawn_folder) 659 | os.makedirs(spawn_folder) 660 | return spawn_folder 661 | 662 | def make_time_stamp(self): 663 | tmp_file_fn = time.strftime('%Y%m%d%H%M%S', 664 | time.localtime(time.time())) 665 | return tmp_file_fn 666 | 667 | def get_snapshot_time_path(self): 668 | snapshot_time_path = os.path.join(self._get_snapshot_path(), 669 | self.make_time_stamp()) 670 | if not os.path.exists(snapshot_time_path): 671 | LOG.debug(_('Creating folder %s for image bundle temp file') % 672 | snapshot_time_path) 673 | os.makedirs(snapshot_time_path) 674 | return snapshot_time_path 675 | 676 | def clean_temp_folder(self, tmp_file_fn): 677 | if os.path.isdir(tmp_file_fn): 678 | LOG.debug(_('Removing existing folder %s '), tmp_file_fn) 679 | shutil.rmtree(tmp_file_fn) 680 | 681 | def _get_instances_path(self): 682 | return os.path.normpath(CONF.instances_path) 683 | 684 | def get_instance_path(self, os_node, instance_name): 685 | instance_folder = os.path.join(self._get_instances_path(), os_node, 686 | instance_name) 687 | if not os.path.exists(instance_folder): 688 | LOG.debug(_("Creating the instance path %s") % instance_folder) 689 | os.makedirs(instance_folder) 690 | return instance_folder 691 | 692 | def get_console_log_path(self, os_node, instance_name): 693 | return os.path.join(self.get_instance_path(os_node, instance_name), 694 | "console.log") 695 | 696 | 697 | class NetworkUtils(object): 698 | """Utilities for z/VM network operator.""" 699 | 700 | def validate_ip_address(self, ip_address): 701 | """Check whether ip_address is valid.""" 702 | # TODO(Leon): check IP address format 703 | pass 704 | 705 | def validate_network_mask(self, mask): 706 | """Check whether mask is valid.""" 707 | # TODO(Leon): check network mask format 708 | pass 709 | 710 | def create_network_configuration_files(self, file_path, network_info, 711 | base_vdev, os_type): 712 | """Generate network configuration files to instance.""" 713 | device_num = 0 714 | cfg_files = [] 715 | cmd_strings = '' 716 | udev_cfg_str = '' 717 | dns_cfg_str = '' 718 | route_cfg_str = '' 719 | cmd_str = None 720 | cfg_str = '' 721 | # Red Hat 722 | file_path_rhel = '/etc/sysconfig/network-scripts/' 723 | # SuSE 724 | file_path_sles = '/etc/sysconfig/network/' 725 | file_name_route = file_path_sles + 'routes' 726 | 727 | # Check the OS type 728 | if (os_type == 'sles'): 729 | file_path = file_path_sles 730 | else: 731 | file_path = file_path_rhel 732 | file_name_dns = '/etc/resolv.conf' 733 | for vif in network_info: 734 | file_name = 'ifcfg-eth' + str(device_num) 735 | network = vif['network'] 736 | (cfg_str, cmd_str, dns_str, route_str) =\ 737 | self.generate_network_configration(network, 738 | base_vdev, device_num, os_type) 739 | LOG.debug(_('Network configure file content is: %s') % cfg_str) 740 | target_net_conf_file_name = file_path + file_name 741 | cfg_files.append((target_net_conf_file_name, cfg_str)) 742 | udev_cfg_str += self.generate_udev_configuration(device_num, 743 | '0.0.' + str(base_vdev).zfill(4)) 744 | if cmd_str is not None: 745 | cmd_strings += cmd_str 746 | if len(dns_str) > 0: 747 | dns_cfg_str += dns_str 748 | if len(route_str) > 0: 749 | route_cfg_str += route_str 750 | base_vdev = str(hex(int(base_vdev, 16) + 3))[2:] 751 | device_num += 1 752 | 753 | if len(dns_cfg_str) > 0: 754 | cfg_files.append((file_name_dns, dns_cfg_str)) 755 | if os_type == 'sles': 756 | udev_file_name = '/etc/udev/rules.d/70-persistent-net.rules' 757 | cfg_files.append((udev_file_name, udev_cfg_str)) 758 | if len(route_cfg_str) > 0: 759 | cfg_files.append((file_name_route, route_cfg_str)) 760 | 761 | return cfg_files, cmd_strings 762 | 763 | def generate_network_configration(self, network, vdev, device_num, 764 | os_type): 765 | """Generate network configuration items.""" 766 | ip_v4 = netmask_v4 = gateway_v4 = broadcast_v4 = '' 767 | subchannels = None 768 | device = None 769 | cidr_v4 = None 770 | cmd_str = None 771 | dns_str = '' 772 | route_str = '' 773 | 774 | subnets_v4 = [s for s in network['subnets'] if s['version'] == 4] 775 | 776 | if len(subnets_v4[0]['ips']) > 0: 777 | ip_v4 = subnets_v4[0]['ips'][0]['address'] 778 | if len(subnets_v4[0]['dns']) > 0: 779 | for dns in subnets_v4[0]['dns']: 780 | dns_str += 'nameserver ' + dns['address'] + '\n' 781 | 782 | netmask_v4 = str(subnets_v4[0].as_netaddr().netmask) 783 | gateway_v4 = subnets_v4[0]['gateway']['address'] 784 | broadcast_v4 = str(subnets_v4[0].as_netaddr().broadcast) 785 | device = "eth" + str(device_num) 786 | address_read = str(vdev).zfill(4) 787 | address_write = str(hex(int(vdev, 16) + 1))[2:].zfill(4) 788 | address_data = str(hex(int(vdev, 16) + 2))[2:].zfill(4) 789 | subchannels = '0.0.%s' % address_read.lower() 790 | subchannels += ',0.0.%s' % address_write.lower() 791 | subchannels += ',0.0.%s' % address_data.lower() 792 | 793 | cfg_str = 'DEVICE=\"' + device + '\"\n' + 'BOOTPROTO=\"static\"\n' 794 | cfg_str += 'BROADCAST=\"' + broadcast_v4 + '\"\n' 795 | cfg_str += 'GATEWAY=\"' + gateway_v4 + '\"\nIPADDR=\"' + ip_v4 + '\"\n' 796 | cfg_str += 'NETMASK=\"' + netmask_v4 + '\"\n' 797 | cfg_str += 'NETTYPE=\"qeth\"\nONBOOT=\"yes\"\n' 798 | cfg_str += 'PORTNAME=\"PORT' + address_read + '\"\n' 799 | cfg_str += 'OPTIONS=\"layer2=1\"\n' 800 | cfg_str += 'SUBCHANNELS=\"' + subchannels + '\"\n' 801 | 802 | if os_type == 'sles': 803 | cidr_v4 = self._get_cidr_from_ip_netmask(ip_v4, netmask_v4) 804 | cmd_str = 'qeth_configure -l 0.0.%s ' % address_read.lower() 805 | cmd_str += '0.0.%(write)s 0.0.%(data)s 1\n' % {'write': 806 | address_write.lower(), 'data': address_data.lower()} 807 | cfg_str = "BOOTPROTO=\'static\'\nIPADDR=\'%s\'\n" % cidr_v4 808 | cfg_str += "BROADCAST=\'%s\'\n" % broadcast_v4 809 | cfg_str += "STARTMODE=\'onboot\'\n" 810 | cfg_str += "NAME=\'OSA Express Network card (%s)\'\n" %\ 811 | address_read 812 | route_str += 'default %s - -\n' % gateway_v4 813 | 814 | return cfg_str, cmd_str, dns_str, route_str 815 | 816 | def generate_udev_configuration(self, device, dev_channel): 817 | cfg_str = 'SUBSYSTEM==\"net\", ACTION==\"add\", DRIVERS==\"qeth\",' 818 | cfg_str += ' KERNELS==\"%s\", ATTR{type}==\"1\",' % dev_channel 819 | cfg_str += ' KERNEL==\"eth*\", NAME=\"eth%s\"\n' % device 820 | 821 | return cfg_str 822 | 823 | def _get_cidr_from_ip_netmask(self, ip, netmask): 824 | netmask_fields = netmask.split('.') 825 | bin_str = '' 826 | for octet in netmask_fields: 827 | bin_str += bin(int(octet))[2:].zfill(8) 828 | mask = str(len(bin_str.rstrip('0'))) 829 | cidr_v4 = ip + '/' + mask 830 | return cidr_v4 831 | -------------------------------------------------------------------------------- /nova-zvm-virt-driver/nova/virt/zvm/vif.py: -------------------------------------------------------------------------------- 1 | # vim: tabstop=4 shiftwidth=4 softtabstop=4 2 | 3 | # Copyright 2013 IBM Corp. 4 | # 5 | # Licensed under the Apache License, Version 2.0 (the "License"); you may 6 | # not use this file except in compliance with the License. You may obtain 7 | # a copy of the License at 8 | # 9 | # http://www.apache.org/licenses/LICENSE-2.0 10 | # 11 | # Unless required by applicable law or agreed to in writing, software 12 | # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 13 | # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 14 | # License for the specific language governing permissions and limitations 15 | # under the License. 16 | 17 | 18 | import abc 19 | 20 | from nova.openstack.common import log as logging 21 | 22 | 23 | LOG = logging.getLogger(__name__) 24 | 25 | 26 | class ZVMBaseVIFDriver(object): 27 | @abc.abstractmethod 28 | def plug(self, instance, vif): 29 | pass 30 | 31 | @abc.abstractmethod 32 | def unplug(self, instance, vif): 33 | pass 34 | 35 | 36 | class ZVMNeutronVIFDriver(ZVMBaseVIFDriver): 37 | """Neutron VIF driver.""" 38 | 39 | def plug(self, instance, vif): 40 | # Neutron takes care of plugging the port 41 | pass 42 | 43 | def unplug(self, instance, vif): 44 | # Neutron takes care of unplugging the port 45 | pass 46 | -------------------------------------------------------------------------------- /nova-zvm-virt-driver/nova/virt/zvm/volumeop.py: -------------------------------------------------------------------------------- 1 | # Copyright 2013 IBM Corp. 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); you may 4 | # not use this file except in compliance with the License. You may obtain 5 | # a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 11 | # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 12 | # License for the specific language governing permissions and limitations 13 | # under the License. 14 | 15 | import contextlib 16 | import os 17 | from random import randint 18 | import re 19 | import time 20 | 21 | from oslo.config import cfg 22 | 23 | import nova.context 24 | from nova.objects import block_device as block_device_obj 25 | from nova.objects import instance as instance_obj 26 | from nova.openstack.common.gettextutils import _ 27 | from nova.openstack.common import jsonutils 28 | from nova.openstack.common import log as logging 29 | from nova.virt.zvm import exception 30 | from nova.virt.zvm import utils as zvmutils 31 | from nova import volume 32 | 33 | 34 | LOG = logging.getLogger(__name__) 35 | CONF = cfg.CONF 36 | 37 | 38 | class VolumeOperator(object): 39 | """Volume operator on IBM z/VM platform.""" 40 | 41 | def __init__(self): 42 | self._svc_driver = SVCDriver() 43 | 44 | def init_host(self, host_stats): 45 | try: 46 | self._svc_driver.init_host(host_stats) 47 | except (exception.ZVMDriverError, exception.ZVMVolumeError) as err: 48 | LOG.warning(_("Initialize zhcp failed. Reason: %s") % err) 49 | 50 | def attach_volume_to_instance(self, context, connection_info, instance, 51 | mountpoint, is_active, rollback=True): 52 | """Attach a volume to an instance.""" 53 | 54 | if None in [connection_info, instance, is_active]: 55 | errmsg = _("Missing required parameters.") 56 | raise exception.ZVMDriverError(msg=errmsg) 57 | 58 | LOG.debug(_("Attach a volume to an instance. conn_info: %(info)s; " + 59 | "instance: %(name)s; mountpoint: %(point)s") % 60 | {'info': connection_info, 'name': instance['name'], 61 | 'point': mountpoint}) 62 | 63 | if is_active: 64 | self._svc_driver.attach_volume_active(context, connection_info, 65 | instance, mountpoint, 66 | rollback) 67 | else: 68 | self._svc_driver.attach_volume_inactive(context, connection_info, 69 | instance, mountpoint, 70 | rollback) 71 | 72 | def detach_volume_from_instance(self, connection_info, instance, 73 | mountpoint, is_active, rollback=True): 74 | """Detach a volume from an instance.""" 75 | 76 | if None in [connection_info, instance, is_active]: 77 | errmsg = _("Missing required parameters.") 78 | raise exception.ZVMDriverError(msg=errmsg) 79 | 80 | LOG.debug(_("Detach a volume from an instance. conn_info: %(info)s; " + 81 | "instance: %(name)s; mountpoint: %(point)s") % 82 | {'info': connection_info, 'name': instance['name'], 83 | 'point': mountpoint}) 84 | 85 | if is_active: 86 | self._svc_driver.detach_volume_active(connection_info, instance, 87 | mountpoint, rollback) 88 | else: 89 | self._svc_driver.detach_volume_inactive(connection_info, instance, 90 | mountpoint, rollback) 91 | 92 | def get_volume_connector(self, instance): 93 | if not instance: 94 | errmsg = _("Instance must be provided.") 95 | raise exception.ZVMDriverError(msg=errmsg) 96 | return self._svc_driver.get_volume_connector(instance) 97 | 98 | def has_persistent_volume(self, instance): 99 | if not instance: 100 | errmsg = _("Instance must be provided.") 101 | raise exception.ZVMDriverError(msg=errmsg) 102 | return self._svc_driver.has_persistent_volume(instance) 103 | 104 | 105 | @contextlib.contextmanager 106 | def wrap_internal_errors(): 107 | """Wrap internal exceptions to ZVMVolumeError.""" 108 | 109 | try: 110 | yield 111 | except exception.ZVMBaseException: 112 | raise 113 | except Exception as err: 114 | raise exception.ZVMVolumeError(msg=err) 115 | 116 | 117 | class DriverAPI(object): 118 | """DriverAPI for implement volume_attach on IBM z/VM platform.""" 119 | 120 | def init_host(self, host_stats): 121 | """Initialize host environment.""" 122 | raise NotImplementedError 123 | 124 | def get_volume_connector(self, instance): 125 | """Get volume connector for current driver.""" 126 | raise NotImplementedError 127 | 128 | def attach_volume_active(self, context, connection_info, instance, 129 | mountpoint, rollback): 130 | """Attach a volume to an running instance.""" 131 | raise NotImplementedError 132 | 133 | def detach_volume_active(self, connection_info, instance, mountpoint, 134 | rollback): 135 | """Detach a volume from an running instance.""" 136 | raise NotImplementedError 137 | 138 | def attach_volume_inactive(self, context, connection_info, instance, 139 | mountpoint, rollback): 140 | """Attach a volume to an shutdown instance.""" 141 | raise NotImplementedError 142 | 143 | def detach_volume_inactive(self, connection_info, instance, mountpoint, 144 | rollback): 145 | """Detach a volume from an shutdown instance.""" 146 | raise NotImplementedError 147 | 148 | def has_persistent_volume(self, instance): 149 | """Decide if the specified instance has persistent volumes attached.""" 150 | raise NotImplementedError 151 | 152 | 153 | class SVCDriver(DriverAPI): 154 | """SVC volume operator on IBM z/VM platform.""" 155 | 156 | def __init__(self): 157 | self._xcat_url = zvmutils.XCATUrl() 158 | self._path_utils = zvmutils.PathUtils() 159 | self._host = CONF.zvm_host 160 | self._pool_name = CONF.zvm_scsi_pool 161 | self._fcp_pool = set() 162 | self._instance_fcp_map = {} 163 | self._is_instance_fcp_map_locked = False 164 | self._volume_api = volume.API() 165 | 166 | self._actions = {'attach_volume': 'addScsiVolume', 167 | 'detach_volume': 'removeScsiVolume', 168 | 'create_mountpoint': 'createfilesysnode', 169 | 'remove_mountpoint': 'removefilesysnode'} 170 | 171 | self._RESERVE = 0 172 | self._INCREASE = 1 173 | self._DECREASE = 2 174 | self._REMOVE = 3 175 | 176 | def init_host(self, host_stats): 177 | """Initialize host environment.""" 178 | 179 | if not host_stats: 180 | errmsg = _("Can not obtain host stats.") 181 | raise exception.ZVMDriverError(msg=errmsg) 182 | 183 | zhcp_fcp_list = CONF.zvm_zhcp_fcp_list 184 | fcp_devices = self._expand_fcp_list(zhcp_fcp_list) 185 | hcpnode = host_stats[0]['zhcp']['nodename'] 186 | for _fcp in fcp_devices: 187 | with zvmutils.ignore_errors(): 188 | self._attach_device(hcpnode, _fcp) 189 | with zvmutils.ignore_errors(): 190 | self._online_device(hcpnode, _fcp) 191 | 192 | fcp_list = CONF.zvm_fcp_list 193 | if (fcp_list is None): 194 | errmsg = _("At least one fcp list should be given") 195 | LOG.error(errmsg) 196 | raise exception.ZVMVolumeError(msg=errmsg) 197 | self._init_fcp_pool(fcp_list) 198 | 199 | def _init_fcp_pool(self, fcp_list): 200 | """Map all instances and their fcp devices, and record all free fcps. 201 | One instance should use only one fcp device so far. 202 | """ 203 | 204 | self._fcp_pool = self._expand_fcp_list(fcp_list) 205 | self._instance_fcp_map = {} 206 | # Any other functions should not modify _instance_fcp_map during 207 | # FCP pool initialization 208 | self._is_instance_fcp_map_locked = True 209 | 210 | compute_host_bdms = self._get_host_volume_bdms() 211 | for instance_bdms in compute_host_bdms: 212 | instance_name = instance_bdms['instance']['name'] 213 | 214 | for _bdm in instance_bdms['instance_bdms']: 215 | connection_info = self._build_connection_info(_bdm) 216 | try: 217 | _fcp = connection_info['data']['zvm_fcp'] 218 | if _fcp and _fcp in self._fcp_pool: 219 | self._update_instance_fcp_map(instance_name, _fcp, 220 | self._INCREASE) 221 | if _fcp and _fcp not in self._fcp_pool: 222 | errmsg = _("FCP device %s is not configured but " + 223 | "is used by %s.") % (_fcp, instance_name) 224 | LOG.warning(errmsg) 225 | except (TypeError, KeyError): 226 | pass 227 | 228 | for _key in self._instance_fcp_map.keys(): 229 | fcp = self._instance_fcp_map.get(_key)['fcp'] 230 | self._fcp_pool.remove(fcp) 231 | self._is_instance_fcp_map_locked = False 232 | 233 | def _update_instance_fcp_map_if_unlocked(self, instance_name, fcp, action): 234 | while self._is_instance_fcp_map_locked: 235 | time.sleep(1) 236 | self._update_instance_fcp_map(instance_name, fcp, action) 237 | 238 | def _update_instance_fcp_map(self, instance_name, fcp, action): 239 | fcp = fcp.lower() 240 | if instance_name in self._instance_fcp_map: 241 | # One instance should use only one fcp device so far 242 | current_fcp = self._instance_fcp_map.get(instance_name)['fcp'] 243 | if fcp != current_fcp: 244 | errmsg = _("Instance %(ins_name)s has multiple FCP devices " + 245 | "attached! FCP1: %(fcp1)s, FCP2: %(fcp2)s" 246 | ) % {'ins_name': instance_name, 'fcp1': fcp, 247 | 'fcp2': current_fcp} 248 | LOG.warning(errmsg) 249 | return 250 | 251 | if action == self._RESERVE: 252 | if instance_name in self._instance_fcp_map: 253 | count = self._instance_fcp_map[instance_name]['count'] 254 | if count > 0: 255 | errmsg = _("Try to reserve a fcp device which already " + 256 | "has volumes attached on: %(ins_name)s:%(fcp)s" 257 | ) % {'ins_name': instance_name, 'fcp': fcp} 258 | LOG.warning(errmsg) 259 | else: 260 | new_item = {instance_name: {'fcp': fcp, 'count': 0}} 261 | self._instance_fcp_map.update(new_item) 262 | 263 | elif action == self._INCREASE: 264 | if instance_name in self._instance_fcp_map: 265 | count = self._instance_fcp_map[instance_name]['count'] 266 | new_item = {instance_name: {'fcp': fcp, 'count': count + 1}} 267 | self._instance_fcp_map.update(new_item) 268 | else: 269 | new_item = {instance_name: {'fcp': fcp, 'count': 1}} 270 | self._instance_fcp_map.update(new_item) 271 | 272 | elif action == self._DECREASE: 273 | if instance_name in self._instance_fcp_map: 274 | count = self._instance_fcp_map[instance_name]['count'] 275 | if count > 0: 276 | new_item = {instance_name: {'fcp': fcp, 277 | 'count': count - 1}} 278 | self._instance_fcp_map.update(new_item) 279 | else: 280 | fcp = self._instance_fcp_map[instance_name]['fcp'] 281 | self._instance_fcp_map.pop(instance_name) 282 | self._fcp_pool.add(fcp) 283 | else: 284 | errmsg = _("Try to decrease an inexistent map item: " + 285 | "%(ins_name)s:%(fcp)s" 286 | ) % {'ins_name': instance_name, 'fcp': fcp} 287 | LOG.warning(errmsg) 288 | 289 | elif action == self._REMOVE: 290 | if instance_name in self._instance_fcp_map: 291 | count = self._instance_fcp_map[instance_name]['count'] 292 | if count > 0: 293 | errmsg = _("Try to remove a map item while some volumes " + 294 | "are still attached on: %(ins_name)s:%(fcp)s" 295 | ) % {'ins_name': instance_name, 'fcp': fcp} 296 | LOG.warning(errmsg) 297 | else: 298 | fcp = self._instance_fcp_map[instance_name]['fcp'] 299 | self._instance_fcp_map.pop(instance_name) 300 | self._fcp_pool.add(fcp) 301 | else: 302 | errmsg = _("Try to remove an inexistent map item: " + 303 | "%(ins_name)s:%(fcp)s" 304 | ) % {'ins_name': instance_name, 'fcp': fcp} 305 | LOG.warning(errmsg) 306 | 307 | else: 308 | errmsg = _("Unrecognized option: %s") % action 309 | LOG.warning(errmsg) 310 | 311 | def _get_host_volume_bdms(self): 312 | """Return all block device mappings on a compute host.""" 313 | 314 | compute_host_bdms = [] 315 | instances = self._get_all_instances() 316 | for instance in instances: 317 | instance_bdms = self._get_instance_bdms(instance) 318 | compute_host_bdms.append(dict(instance=instance, 319 | instance_bdms=instance_bdms)) 320 | 321 | return compute_host_bdms 322 | 323 | def _get_all_instances(self): 324 | context = nova.context.get_admin_context() 325 | return instance_obj.InstanceList.get_by_host(context, self._host) 326 | 327 | def _get_instance_bdms(self, instance): 328 | context = nova.context.get_admin_context() 329 | instance_bdms = [bdm for bdm in 330 | (block_device_obj.BlockDeviceMappingList. 331 | get_by_instance_uuid(context, instance['uuid'])) 332 | if bdm.is_volume] 333 | return instance_bdms 334 | 335 | def has_persistent_volume(self, instance): 336 | return bool(self._get_instance_bdms(instance)) 337 | 338 | def _build_connection_info(self, bdm): 339 | try: 340 | connection_info = jsonutils.loads(bdm['connection_info']) 341 | return connection_info 342 | except (TypeError, KeyError, ValueError): 343 | return None 344 | 345 | def get_volume_connector(self, instance): 346 | try: 347 | fcp = self._instance_fcp_map.get(instance['name'])['fcp'] 348 | except Exception: 349 | fcp = None 350 | if not fcp: 351 | fcp = self._get_fcp_from_pool() 352 | if fcp: 353 | self._update_instance_fcp_map_if_unlocked(instance['name'], 354 | fcp, self._RESERVE) 355 | 356 | if not fcp: 357 | errmsg = _("No available FCP device found.") 358 | LOG.error(errmsg) 359 | raise exception.ZVMVolumeError(msg=errmsg) 360 | fcp = fcp.lower() 361 | 362 | wwpn = self._get_wwpn(fcp) 363 | if not wwpn: 364 | errmsg = _("FCP device %s has no available WWPN.") % fcp 365 | LOG.error(errmsg) 366 | raise exception.ZVMVolumeError(msg=errmsg) 367 | wwpn = wwpn.lower() 368 | 369 | return {'zvm_fcp': fcp, 'wwpns': [wwpn], 'host': CONF.zvm_host} 370 | 371 | def _get_wwpn(self, fcp): 372 | states = ['active', 'free'] 373 | for _state in states: 374 | fcps_info = self._list_fcp_details(_state) 375 | if not fcps_info: 376 | continue 377 | wwpn = self._extract_wwpn_from_fcp_info(fcp, fcps_info) 378 | if wwpn: 379 | return wwpn 380 | 381 | def _list_fcp_details(self, state): 382 | fields = '&field=--fcpdevices&field=' + state + '&field=details' 383 | rsp = self._xcat_rinv(fields) 384 | try: 385 | fcp_details = rsp['info'][0][0].splitlines() 386 | return fcp_details 387 | except (TypeError, KeyError): 388 | return None 389 | 390 | def _extract_wwpn_from_fcp_info(self, fcp, fcps_info): 391 | """The FCP infomation would look like this: 392 | host: FCP device number: xxxx 393 | host: Status: Active 394 | host: NPIV world wide port number: xxxxxxxx 395 | host: Channel path ID: xx 396 | host: Physical world wide port number: xxxxxxxx 397 | ...... 398 | host: FCP device number: xxxx 399 | host: Status: Active 400 | host: NPIV world wide port number: xxxxxxxx 401 | host: Channel path ID: xx 402 | host: Physical world wide port number: xxxxxxxx 403 | 404 | """ 405 | 406 | lines_per_item = 5 407 | num_fcps = len(fcps_info) / lines_per_item 408 | fcp = fcp.upper() 409 | for _cur in range(0, num_fcps): 410 | # Find target FCP device 411 | if fcp not in fcps_info[_cur * lines_per_item]: 412 | continue 413 | # Try to get NPIV WWPN first 414 | wwpn_info = fcps_info[(_cur + 1) * lines_per_item - 3] 415 | wwpn = self._get_wwpn_from_line(wwpn_info) 416 | if not wwpn: 417 | # Get physical WWPN if NPIV WWPN is none 418 | wwpn_info = fcps_info[(_cur + 1) * lines_per_item - 1] 419 | wwpn = self._get_wwpn_from_line(wwpn_info) 420 | return wwpn 421 | 422 | def _get_wwpn_from_line(self, info_line): 423 | wwpn = info_line.split(':')[-1].strip() 424 | if wwpn and wwpn.upper() != 'NONE': 425 | return wwpn 426 | else: 427 | return None 428 | 429 | def _get_fcp_from_pool(self): 430 | if self._fcp_pool: 431 | return self._fcp_pool.pop() 432 | 433 | self._init_fcp_pool(CONF.zvm_fcp_list) 434 | if self._fcp_pool: 435 | return self._fcp_pool.pop() 436 | 437 | def _extract_connection_info(self, context, connection_info): 438 | with wrap_internal_errors(): 439 | LOG.debug(_("Extract connection_info: %s") % connection_info) 440 | 441 | lun = connection_info['data']['target_lun'] 442 | lun = "%04x000000000000" % int(lun) 443 | wwpn = connection_info['data']['target_wwn'] 444 | size = '0G' 445 | # There is no context in detach case 446 | if context: 447 | volume_id = connection_info['data']['volume_id'] 448 | volume = self._get_volume_by_id(context, volume_id) 449 | size = str(volume['size']) + 'G' 450 | fcp = connection_info['data']['zvm_fcp'] 451 | 452 | return (lun.lower(), wwpn.lower(), size, fcp.lower()) 453 | 454 | def _get_volume_by_id(self, context, volume_id): 455 | volume = self._volume_api.get(context, volume_id) 456 | return volume 457 | 458 | def attach_volume_active(self, context, connection_info, instance, 459 | mountpoint, rollback=True): 460 | """Attach a volume to an running instance.""" 461 | 462 | (lun, wwpn, size, fcp) = self._extract_connection_info(context, 463 | connection_info) 464 | try: 465 | self._update_instance_fcp_map_if_unlocked(instance['name'], fcp, 466 | self._INCREASE) 467 | self._add_zfcp_to_pool(fcp, wwpn, lun, size) 468 | self._add_zfcp(instance, fcp, wwpn, lun, size) 469 | if mountpoint: 470 | self._create_mountpoint(instance, fcp, wwpn, lun, mountpoint) 471 | except (exception.ZVMXCATRequestFailed, 472 | exception.ZVMInvalidXCATResponseDataError, 473 | exception.ZVMXCATInternalError, 474 | exception.ZVMVolumeError): 475 | self._update_instance_fcp_map_if_unlocked(instance['name'], fcp, 476 | self._DECREASE) 477 | do_detach = not self._is_fcp_in_use(instance, fcp) 478 | if rollback: 479 | with zvmutils.ignore_errors(): 480 | self._remove_mountpoint(instance, mountpoint) 481 | with zvmutils.ignore_errors(): 482 | self._remove_zfcp(instance, fcp, wwpn, lun) 483 | with zvmutils.ignore_errors(): 484 | self._remove_zfcp_from_pool(wwpn, lun) 485 | with zvmutils.ignore_errors(): 486 | if do_detach: 487 | self._detach_device(instance['name'], fcp) 488 | raise 489 | 490 | def detach_volume_active(self, connection_info, instance, mountpoint, 491 | rollback=True): 492 | """Detach a volume from an running instance.""" 493 | 494 | (lun, wwpn, size, fcp) = self._extract_connection_info(None, 495 | connection_info) 496 | try: 497 | self._update_instance_fcp_map_if_unlocked(instance['name'], fcp, 498 | self._DECREASE) 499 | do_detach = not self._is_fcp_in_use(instance, fcp) 500 | if mountpoint: 501 | self._remove_mountpoint(instance, mountpoint) 502 | self._remove_zfcp(instance, fcp, wwpn, lun) 503 | self._remove_zfcp_from_pool(wwpn, lun) 504 | if do_detach: 505 | self._detach_device(instance['name'], fcp) 506 | self._update_instance_fcp_map_if_unlocked(instance['name'], 507 | fcp, self._REMOVE) 508 | except (exception.ZVMXCATRequestFailed, 509 | exception.ZVMInvalidXCATResponseDataError, 510 | exception.ZVMXCATInternalError, 511 | exception.ZVMVolumeError): 512 | self._update_instance_fcp_map_if_unlocked(instance['name'], fcp, 513 | self._INCREASE) 514 | if rollback: 515 | with zvmutils.ignore_errors(): 516 | self._add_zfcp_to_pool(fcp, wwpn, lun, size) 517 | with zvmutils.ignore_errors(): 518 | self._add_zfcp(instance, fcp, wwpn, lun, size) 519 | if mountpoint: 520 | with zvmutils.ignore_errors(): 521 | self._create_mountpoint(instance, fcp, wwpn, lun, 522 | mountpoint) 523 | raise 524 | 525 | def attach_volume_inactive(self, context, connection_info, instance, 526 | mountpoint, rollback=True): 527 | """Attach a volume to an shutdown instance.""" 528 | 529 | (lun, wwpn, size, fcp) = self._extract_connection_info(context, 530 | connection_info) 531 | try: 532 | do_attach = not self._is_fcp_in_use(instance, fcp) 533 | self._update_instance_fcp_map_if_unlocked(instance['name'], fcp, 534 | self._INCREASE) 535 | self._add_zfcp_to_pool(fcp, wwpn, lun, size) 536 | self._allocate_zfcp(instance, fcp, size, wwpn, lun) 537 | self._notice_attach(instance, fcp, wwpn, lun, mountpoint) 538 | if do_attach: 539 | self._attach_device(instance['name'], fcp) 540 | except (exception.ZVMXCATRequestFailed, 541 | exception.ZVMInvalidXCATResponseDataError, 542 | exception.ZVMXCATInternalError, 543 | exception.ZVMVolumeError): 544 | self._update_instance_fcp_map_if_unlocked(instance['name'], fcp, 545 | self._DECREASE) 546 | do_detach = not self._is_fcp_in_use(instance, fcp) 547 | if rollback: 548 | with zvmutils.ignore_errors(): 549 | self._notice_detach(instance, fcp, wwpn, lun, mountpoint) 550 | with zvmutils.ignore_errors(): 551 | self._remove_zfcp_from_pool(wwpn, lun) 552 | with zvmutils.ignore_errors(): 553 | if do_detach: 554 | self._detach_device(instance['name'], fcp) 555 | self._update_instance_fcp_map_if_unlocked( 556 | instance['name'], fcp, self._REMOVE) 557 | raise 558 | 559 | def detach_volume_inactive(self, connection_info, instance, mountpoint, 560 | rollback=True): 561 | """Detach a volume from an shutdown instance.""" 562 | 563 | (lun, wwpn, size, fcp) = self._extract_connection_info(None, 564 | connection_info) 565 | try: 566 | self._update_instance_fcp_map_if_unlocked(instance['name'], fcp, 567 | self._DECREASE) 568 | do_detach = not self._is_fcp_in_use(instance, fcp) 569 | self._remove_zfcp(instance, fcp, wwpn, lun) 570 | self._remove_zfcp_from_pool(wwpn, lun) 571 | self._notice_detach(instance, fcp, wwpn, lun, mountpoint) 572 | if do_detach: 573 | self._detach_device(instance['name'], fcp) 574 | self._update_instance_fcp_map_if_unlocked(instance['name'], 575 | fcp, self._REMOVE) 576 | except (exception.ZVMXCATRequestFailed, 577 | exception.ZVMInvalidXCATResponseDataError, 578 | exception.ZVMXCATInternalError, 579 | exception.ZVMVolumeError): 580 | self._update_instance_fcp_map_if_unlocked(instance['name'], fcp, 581 | self._INCREASE) 582 | if rollback: 583 | with zvmutils.ignore_errors(): 584 | self._attach_device(instance['name'], fcp) 585 | with zvmutils.ignore_errors(): 586 | self._notice_attach(instance, fcp, wwpn, lun, mountpoint) 587 | with zvmutils.ignore_errors(): 588 | self._add_zfcp_to_pool(fcp, wwpn, lun, size) 589 | with zvmutils.ignore_errors(): 590 | self._allocate_zfcp(instance, fcp, size, wwpn, lun) 591 | raise 592 | 593 | def _expand_fcp_list(self, fcp_list): 594 | """Expand fcp list string into a python list object which contains 595 | each fcp devices in the list string. A fcp list is composed of fcp 596 | device addresses, range indicator '-', and split indicator ';'. 597 | 598 | For example, if fcp_list is 599 | "0011-0013;0015;0017-0018", expand_fcp_list(fcp_list) will return 600 | [0011, 0012, 0013, 0015, 0017, 0018]. 601 | 602 | """ 603 | 604 | LOG.debug(_("Expand FCP list %s") % fcp_list) 605 | 606 | if not fcp_list: 607 | return set() 608 | 609 | range_pattern = '[0-9a-fA-F]{1,4}(-[0-9a-fA-F]{1,4})?' 610 | match_pattern = "^(%(range)s)(;%(range)s)*$" % {'range': range_pattern} 611 | if not re.match(match_pattern, fcp_list): 612 | errmsg = _("Invalid FCP address %s") % fcp_list 613 | raise exception.ZVMDriverError(msg=errmsg) 614 | 615 | fcp_devices = set() 616 | for _range in fcp_list.split(';'): 617 | if '-' not in _range: 618 | # single device 619 | fcp_addr = int(_range, 16) 620 | fcp_devices.add("%04x" % fcp_addr) 621 | else: 622 | # a range of address 623 | (_min, _max) = _range.split('-') 624 | _min = int(_min, 16) 625 | _max = int(_max, 16) 626 | for fcp_addr in range(_min, _max + 1): 627 | fcp_devices.add("%04x" % fcp_addr) 628 | 629 | # remove duplicate entries 630 | return fcp_devices 631 | 632 | def _attach_device(self, node, addr, mode='0'): 633 | """Attach a device to a node.""" 634 | 635 | body = [' '.join(['--dedicatedevice', addr, addr, mode])] 636 | self._xcat_chvm(node, body) 637 | 638 | def _detach_device(self, node, vdev): 639 | """Detach a device from a node.""" 640 | 641 | body = [' '.join(['--undedicatedevice', vdev])] 642 | self._xcat_chvm(node, body) 643 | 644 | def _online_device(self, node, dev): 645 | """After attaching a device to a node, the device should be made 646 | online before it being in use. 647 | 648 | """ 649 | 650 | body = ["command=cio_ignore -r %s" % dev] 651 | self._xcat_xdsh(node, body) 652 | 653 | body = ["command=chccwdev -e %s" % dev] 654 | self._xcat_xdsh(node, body) 655 | 656 | def _is_fcp_in_use(self, instance, fcp): 657 | if instance['name'] in self._instance_fcp_map: 658 | count = self._instance_fcp_map.get(instance['name'])['count'] 659 | if count > 0: 660 | return True 661 | return False 662 | 663 | def _notice_attach(self, instance, fcp, wwpn, lun, mountpoint): 664 | file_path = self._get_file_for_punch(instance) 665 | 666 | # Create and send volume file 667 | action = self._actions['attach_volume'] 668 | lines = self._get_volume_lines(action, fcp, wwpn, lun) 669 | self._send_notice(instance, file_path, lines) 670 | 671 | # Create and send mount point file 672 | action = self._actions['create_mountpoint'] 673 | lines = self._get_mountpoint_lines(action, fcp, wwpn, lun, mountpoint) 674 | self._send_notice(instance, file_path, lines) 675 | 676 | def _notice_detach(self, instance, fcp, wwpn, lun, mountpoint): 677 | file_path = self._get_file_for_punch(instance) 678 | 679 | # Create and send volume file 680 | action = self._actions['detach_volume'] 681 | lines = self._get_volume_lines(action, fcp, wwpn, lun) 682 | self._send_notice(instance, file_path, lines) 683 | 684 | # Create and send mount point file 685 | action = self._actions['remove_mountpoint'] 686 | lines = self._get_mountpoint_lines(action, fcp, wwpn, lun, mountpoint) 687 | self._send_notice(instance, file_path, lines) 688 | 689 | def _get_volume_lines(self, action, fcp, wwpn, lun): 690 | comments = '# xCAT Init\n' 691 | action_line = "action=%s\n" % action 692 | fcp_line = "fcpAddr=%s\n" % fcp 693 | wwpn_line = "wwpn=%s\n" % wwpn 694 | lun_line = "lun=%s\n" % lun 695 | return [comments, action_line, fcp_line, wwpn_line, lun_line] 696 | 697 | def _get_mountpoint_lines(self, action, fcp, wwpn, lun, mountpoint): 698 | comments = '# xCAT Init\n' 699 | action_line = "action=%s\n" % action 700 | mountpoint_line = "tgtFile=%s\n" % mountpoint 701 | if action == self._actions['create_mountpoint']: 702 | path = self._get_zfcp_path_pattern() 703 | srcdev = path % {'fcp': fcp, 'wwpn': wwpn, 'lun': lun} 704 | srcdev_line = "srcFile=%s\n" % srcdev 705 | return [comments, action_line, mountpoint_line, srcdev_line] 706 | else: 707 | return [comments, action_line, mountpoint_line] 708 | 709 | def _get_file_for_punch(self, instance): 710 | dir_path = self._path_utils.get_punch_time_path() 711 | file_name = "%08x.disk" % randint(0, int('FFFFFFFF', 16)) 712 | file_path = os.path.join(dir_path, file_name) 713 | return file_path 714 | 715 | def _send_notice(self, instance, file_path, lines): 716 | with wrap_internal_errors(): 717 | with open(file_path, 'w') as f: 718 | f.writelines(lines) 719 | 720 | # zvmutils.punch_file will remove the file after punching 721 | zvmutils.punch_file(instance['name'], file_path, 'X') 722 | 723 | def _add_zfcp_to_pool(self, fcp, wwpn, lun, size): 724 | body = [' '.join(['--addzfcp2pool', self._pool_name, 'free', wwpn, 725 | lun, size, fcp])] 726 | self._xcat_chhy(body) 727 | 728 | def _remove_zfcp_from_pool(self, wwpn, lun): 729 | body = [' '.join(['--removezfcpfrompool', CONF.zvm_scsi_pool, lun, 730 | wwpn])] 731 | self._xcat_chhy(body) 732 | 733 | def _add_zfcp(self, instance, fcp, wwpn, lun, size): 734 | body = [' '.join(['--addzfcp', CONF.zvm_scsi_pool, fcp, str(0), size, 735 | str(0), wwpn, lun])] 736 | self._xcat_chvm(instance['name'], body) 737 | 738 | def _remove_zfcp(self, instance, fcp, wwpn, lun): 739 | body = [' '.join(['--removezfcp', fcp, wwpn, lun, '1'])] 740 | self._xcat_chvm(instance['name'], body) 741 | 742 | def _create_mountpoint(self, instance, fcp, wwpn, lun, mountpoint): 743 | path = self._get_zfcp_path_pattern() 744 | srcdev = path % {'fcp': fcp, 'wwpn': wwpn, 'lun': lun} 745 | body = [" ".join(['--createfilesysnode', srcdev, mountpoint])] 746 | self._xcat_chvm(instance['name'], body) 747 | 748 | def _remove_mountpoint(self, instance, mountpoint): 749 | body = [' '.join(['--removefilesysnode', mountpoint])] 750 | self._xcat_chvm(instance['name'], body) 751 | 752 | def _allocate_zfcp(self, instance, fcp, size, wwpn, lun): 753 | body = [" ".join(['--reservezfcp', CONF.zvm_scsi_pool, 'used', 754 | instance['name'], fcp, size, wwpn, lun])] 755 | self._xcat_chhy(body) 756 | 757 | def _xcat_chvm(self, node, body): 758 | url = self._xcat_url.chvm('/' + node) 759 | zvmutils.xcat_request('PUT', url, body) 760 | 761 | def _xcat_chhy(self, body): 762 | url = self._xcat_url.chhv('/' + self._host) 763 | zvmutils.xcat_request('PUT', url, body) 764 | 765 | def _xcat_xdsh(self, node, body): 766 | url = self._xcat_url.xdsh('/' + node) 767 | zvmutils.xcat_request('PUT', url, body) 768 | 769 | def _xcat_rinv(self, fields): 770 | url = self._xcat_url.rinv('/' + self._host, fields) 771 | return zvmutils.xcat_request('GET', url) 772 | 773 | def _get_zfcp_path_pattern(self): 774 | return '/dev/disk/by-path/ccw-0.0.%(fcp)s-zfcp-0x%(wwpn)s:0x%(lun)s' 775 | --------------------------------------------------------------------------------