Adds netapp volume driver
* includes a fake implementation of the netapp soap api * implements blueprint netapp-volume-driver Change-Id: I8e2781e8bd4a552354f46a32c4a7ecdfd19f3e7a
This commit is contained in:
parent
36100f6f99
commit
3b7fa6b605
2
Authors
2
Authors
@ -18,6 +18,7 @@ Armando Migliaccio <Armando.Migliaccio@eu.citrix.com>
|
||||
Arvind Somya <asomya@cisco.com>
|
||||
Asbjørn Sannes <asbjorn.sannes@interhost.no>
|
||||
Ben McGraw <ben@pistoncloud.com>
|
||||
Ben Swartzlander <bswartz@netapp.com>
|
||||
Bilal Akhtar <bilalakhtar@ubuntu.com>
|
||||
Brad Hall <brad@nicira.com>
|
||||
Brad McConnell <bmcconne@rackspace.com>
|
||||
@ -147,6 +148,7 @@ Ricardo Carrillo Cruz <emaildericky@gmail.com>
|
||||
Rick Clark <rick@openstack.org>
|
||||
Rick Harris <rconradharris@gmail.com>
|
||||
Rob Kost <kost@isi.edu>
|
||||
Robert Esker <esker@netapp.com>
|
||||
Russell Bryant <rbryant@redhat.com>
|
||||
Russell Sim <russell.sim@gmail.com>
|
||||
Ryan Lane <rlane@wikimedia.org>
|
||||
|
929
nova/tests/test_netapp.py
Normal file
929
nova/tests/test_netapp.py
Normal file
@ -0,0 +1,929 @@
|
||||
# vim: tabstop=4 shiftwidth=4 softtabstop=4
|
||||
|
||||
# Copyright (c) 2012 NetApp, Inc.
|
||||
# All Rights Reserved.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License"); you may
|
||||
# not use this file except in compliance with the License. You may obtain
|
||||
# a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
# License for the specific language governing permissions and limitations
|
||||
# under the License.
|
||||
"""
|
||||
Tests for NetApp volume driver
|
||||
|
||||
"""
|
||||
|
||||
import BaseHTTPServer
|
||||
import httplib
|
||||
import StringIO
|
||||
|
||||
from lxml import etree
|
||||
|
||||
from nova import log as logging
|
||||
from nova import test
|
||||
from nova.volume.netapp import NetAppISCSIDriver
|
||||
|
||||
LOG = logging.getLogger("nova.volume.driver")
|
||||
|
||||
|
||||
WSDL_HEADER = """<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<definitions xmlns="http://schemas.xmlsoap.org/wsdl/"
|
||||
xmlns:na="http://www.netapp.com/management/v1"
|
||||
xmlns:soap="http://schemas.xmlsoap.org/wsdl/soap/"
|
||||
xmlns:xsd="http://www.w3.org/2001/XMLSchema" name="NetAppDfm"
|
||||
targetNamespace="http://www.netapp.com/management/v1">"""
|
||||
|
||||
WSDL_TYPES = """<types>
|
||||
<xsd:schema attributeFormDefault="unqualified" elementFormDefault="qualified"
|
||||
targetNamespace="http://www.netapp.com/management/v1">
|
||||
<xsd:element name="ApiProxy">
|
||||
<xsd:complexType>
|
||||
<xsd:all>
|
||||
<xsd:element name="Request" type="na:Request"/>
|
||||
<xsd:element name="Target" type="xsd:string"/>
|
||||
<xsd:element minOccurs="0" name="Timeout" type="xsd:integer"/>
|
||||
<xsd:element minOccurs="0" name="Username" type="xsd:string"/>
|
||||
</xsd:all>
|
||||
</xsd:complexType>
|
||||
</xsd:element>
|
||||
<xsd:element name="ApiProxyResult">
|
||||
<xsd:complexType>
|
||||
<xsd:all>
|
||||
<xsd:element name="Response" type="na:Response"/>
|
||||
</xsd:all>
|
||||
</xsd:complexType>
|
||||
</xsd:element>
|
||||
<xsd:element name="DatasetEditBegin">
|
||||
<xsd:complexType>
|
||||
<xsd:all>
|
||||
<xsd:element name="DatasetNameOrId" type="na:ObjNameOrId"/>
|
||||
<xsd:element minOccurs="0" name="Force" type="xsd:boolean"/>
|
||||
</xsd:all>
|
||||
</xsd:complexType>
|
||||
</xsd:element>
|
||||
<xsd:element name="DatasetEditBeginResult">
|
||||
<xsd:complexType>
|
||||
<xsd:all>
|
||||
<xsd:element name="EditLockId" type="xsd:integer"/>
|
||||
</xsd:all>
|
||||
</xsd:complexType>
|
||||
</xsd:element>
|
||||
<xsd:element name="DatasetEditCommit">
|
||||
<xsd:complexType>
|
||||
<xsd:all>
|
||||
<xsd:element minOccurs="0" name="AssumeConfirmation"
|
||||
type="xsd:boolean"/>
|
||||
<xsd:element name="EditLockId" type="xsd:integer"/>
|
||||
</xsd:all>
|
||||
</xsd:complexType>
|
||||
</xsd:element>
|
||||
<xsd:element name="DatasetEditCommitResult">
|
||||
<xsd:complexType>
|
||||
<xsd:all>
|
||||
<xsd:element minOccurs="0" name="IsProvisioningFailure"
|
||||
type="xsd:boolean"/>
|
||||
<xsd:element minOccurs="0" name="JobIds" type="na:ArrayOfJobInfo"/>
|
||||
</xsd:all>
|
||||
</xsd:complexType>
|
||||
</xsd:element>
|
||||
<xsd:element name="DatasetEditRollback">
|
||||
<xsd:complexType>
|
||||
<xsd:all>
|
||||
<xsd:element name="EditLockId" type="xsd:integer"/>
|
||||
</xsd:all>
|
||||
</xsd:complexType>
|
||||
</xsd:element>
|
||||
<xsd:element name="DatasetEditRollbackResult">
|
||||
<xsd:complexType/>
|
||||
</xsd:element>
|
||||
<xsd:element name="DatasetListInfoIterEnd">
|
||||
<xsd:complexType>
|
||||
<xsd:all>
|
||||
<xsd:element name="Tag" type="xsd:string"/>
|
||||
</xsd:all>
|
||||
</xsd:complexType>
|
||||
</xsd:element>
|
||||
<xsd:element name="DatasetListInfoIterEndResult">
|
||||
<xsd:complexType/>
|
||||
</xsd:element>
|
||||
<xsd:element name="DatasetListInfoIterNext">
|
||||
<xsd:complexType>
|
||||
<xsd:all>
|
||||
<xsd:element name="Maximum" type="xsd:integer"/>
|
||||
<xsd:element name="Tag" type="xsd:string"/>
|
||||
</xsd:all>
|
||||
</xsd:complexType>
|
||||
</xsd:element>
|
||||
<xsd:element name="DatasetListInfoIterNextResult">
|
||||
<xsd:complexType>
|
||||
<xsd:all>
|
||||
<xsd:element name="Datasets" type="na:ArrayOfDatasetInfo"/>
|
||||
<xsd:element name="Records" type="xsd:integer"/>
|
||||
</xsd:all>
|
||||
</xsd:complexType>
|
||||
</xsd:element>
|
||||
<xsd:element name="DatasetListInfoIterStart">
|
||||
<xsd:complexType>
|
||||
<xsd:all>
|
||||
<xsd:element minOccurs="0" name="ObjectNameOrId"
|
||||
type="na:ObjNameOrId"/>
|
||||
</xsd:all>
|
||||
</xsd:complexType>
|
||||
</xsd:element>
|
||||
<xsd:element name="DatasetListInfoIterStartResult">
|
||||
<xsd:complexType>
|
||||
<xsd:all>
|
||||
<xsd:element name="Records" type="xsd:integer"/>
|
||||
<xsd:element name="Tag" type="xsd:string"/>
|
||||
</xsd:all>
|
||||
</xsd:complexType>
|
||||
</xsd:element>
|
||||
<xsd:element name="DatasetMemberListInfoIterEnd">
|
||||
<xsd:complexType>
|
||||
<xsd:all>
|
||||
<xsd:element name="Tag" type="xsd:string"/>
|
||||
</xsd:all>
|
||||
</xsd:complexType>
|
||||
</xsd:element>
|
||||
<xsd:element name="DatasetMemberListInfoIterEndResult">
|
||||
<xsd:complexType/>
|
||||
</xsd:element>
|
||||
<xsd:element name="DatasetMemberListInfoIterNext">
|
||||
<xsd:complexType>
|
||||
<xsd:all>
|
||||
<xsd:element name="Maximum" type="xsd:integer"/>
|
||||
<xsd:element name="Tag" type="xsd:string"/>
|
||||
</xsd:all>
|
||||
</xsd:complexType>
|
||||
</xsd:element>
|
||||
<xsd:element name="DatasetMemberListInfoIterNextResult">
|
||||
<xsd:complexType>
|
||||
<xsd:all>
|
||||
<xsd:element name="DatasetMembers"
|
||||
type="na:ArrayOfDatasetMemberInfo"/>
|
||||
<xsd:element name="Records" type="xsd:integer"/>
|
||||
</xsd:all>
|
||||
</xsd:complexType>
|
||||
</xsd:element>
|
||||
<xsd:element name="DatasetMemberListInfoIterStart">
|
||||
<xsd:complexType>
|
||||
<xsd:all>
|
||||
<xsd:element name="DatasetNameOrId" type="na:ObjNameOrId"/>
|
||||
<xsd:element minOccurs="0" name="IncludeExportsInfo"
|
||||
type="xsd:boolean"/>
|
||||
<xsd:element minOccurs="0" name="IncludeIndirect"
|
||||
type="xsd:boolean"/>
|
||||
<xsd:element minOccurs="0" name="MemberType" type="xsd:string"/>
|
||||
</xsd:all>
|
||||
</xsd:complexType>
|
||||
</xsd:element>
|
||||
<xsd:element name="DatasetMemberListInfoIterStartResult">
|
||||
<xsd:complexType>
|
||||
<xsd:all>
|
||||
<xsd:element name="Records" type="xsd:integer"/>
|
||||
<xsd:element name="Tag" type="xsd:string"/>
|
||||
</xsd:all>
|
||||
</xsd:complexType>
|
||||
</xsd:element>
|
||||
<xsd:element name="DatasetProvisionMember">
|
||||
<xsd:complexType>
|
||||
<xsd:all>
|
||||
<xsd:element name="EditLockId" type="xsd:integer"/>
|
||||
<xsd:element name="ProvisionMemberRequestInfo"
|
||||
type="na:ProvisionMemberRequestInfo"/>
|
||||
</xsd:all>
|
||||
</xsd:complexType>
|
||||
</xsd:element>
|
||||
<xsd:element name="DatasetProvisionMemberResult">
|
||||
<xsd:complexType/>
|
||||
</xsd:element>
|
||||
<xsd:element name="DatasetRemoveMember">
|
||||
<xsd:complexType>
|
||||
<xsd:all>
|
||||
<xsd:element name="DatasetMemberParameters"
|
||||
type="na:ArrayOfDatasetMemberParameter"/>
|
||||
<xsd:element minOccurs="0" name="Destroy" type="xsd:boolean"/>
|
||||
<xsd:element name="EditLockId" type="xsd:integer"/>
|
||||
</xsd:all>
|
||||
</xsd:complexType>
|
||||
</xsd:element>
|
||||
<xsd:element name="DatasetRemoveMemberResult">
|
||||
<xsd:complexType/>
|
||||
</xsd:element>
|
||||
<xsd:element name="DpJobProgressEventListIterEnd">
|
||||
<xsd:complexType>
|
||||
<xsd:all>
|
||||
<xsd:element name="Tag" type="xsd:string"/>
|
||||
</xsd:all>
|
||||
</xsd:complexType>
|
||||
</xsd:element>
|
||||
<xsd:element name="DpJobProgressEventListIterEndResult">
|
||||
<xsd:complexType/>
|
||||
</xsd:element>
|
||||
<xsd:element name="DpJobProgressEventListIterNext">
|
||||
<xsd:complexType>
|
||||
<xsd:all>
|
||||
<xsd:element name="Maximum" type="xsd:integer"/>
|
||||
<xsd:element name="Tag" type="xsd:string"/>
|
||||
</xsd:all>
|
||||
</xsd:complexType>
|
||||
</xsd:element>
|
||||
<xsd:element name="DpJobProgressEventListIterNextResult">
|
||||
<xsd:complexType>
|
||||
<xsd:all>
|
||||
<xsd:element name="ProgressEvents"
|
||||
type="na:ArrayOfDpJobProgressEventInfo"/>
|
||||
<xsd:element name="Records" type="xsd:integer"/>
|
||||
</xsd:all>
|
||||
</xsd:complexType>
|
||||
</xsd:element>
|
||||
<xsd:element name="DpJobProgressEventListIterStart">
|
||||
<xsd:complexType>
|
||||
<xsd:all>
|
||||
<xsd:element minOccurs="0" name="JobId" type="xsd:integer"/>
|
||||
</xsd:all>
|
||||
</xsd:complexType>
|
||||
</xsd:element>
|
||||
<xsd:element name="DpJobProgressEventListIterStartResult">
|
||||
<xsd:complexType>
|
||||
<xsd:all>
|
||||
<xsd:element name="Records" type="xsd:integer"/>
|
||||
<xsd:element name="Tag" type="xsd:string"/>
|
||||
</xsd:all>
|
||||
</xsd:complexType>
|
||||
</xsd:element>
|
||||
<xsd:element name="DfmAbout">
|
||||
<xsd:complexType>
|
||||
<xsd:all>
|
||||
<xsd:element minOccurs="0" name="IncludeDirectorySizeInfo"
|
||||
type="xsd:boolean"/>
|
||||
</xsd:all>
|
||||
</xsd:complexType>
|
||||
</xsd:element>
|
||||
<xsd:element name="DfmAboutResult">
|
||||
<xsd:complexType>
|
||||
<xsd:all/>
|
||||
</xsd:complexType>
|
||||
</xsd:element>
|
||||
<xsd:element name="HostListInfoIterEnd">
|
||||
<xsd:complexType>
|
||||
<xsd:all>
|
||||
<xsd:element name="Tag" type="xsd:string"/>
|
||||
</xsd:all>
|
||||
</xsd:complexType>
|
||||
</xsd:element>
|
||||
<xsd:element name="HostListInfoIterEndResult">
|
||||
<xsd:complexType/>
|
||||
</xsd:element>
|
||||
<xsd:element name="HostListInfoIterNext">
|
||||
<xsd:complexType>
|
||||
<xsd:all>
|
||||
<xsd:element name="Maximum" type="xsd:integer"/>
|
||||
<xsd:element name="Tag" type="xsd:string"/>
|
||||
</xsd:all>
|
||||
</xsd:complexType>
|
||||
</xsd:element>
|
||||
<xsd:element name="HostListInfoIterNextResult">
|
||||
<xsd:complexType>
|
||||
<xsd:all>
|
||||
<xsd:element name="Hosts" type="na:ArrayOfHostInfo"/>
|
||||
<xsd:element name="Records" type="xsd:integer"/>
|
||||
</xsd:all>
|
||||
</xsd:complexType>
|
||||
</xsd:element>
|
||||
<xsd:element name="HostListInfoIterStart">
|
||||
<xsd:complexType>
|
||||
<xsd:all>
|
||||
<xsd:element minOccurs="0" name="ObjectNameOrId"
|
||||
type="na:ObjNameOrId"/>
|
||||
</xsd:all>
|
||||
</xsd:complexType>
|
||||
</xsd:element>
|
||||
<xsd:element name="HostListInfoIterStartResult">
|
||||
<xsd:complexType>
|
||||
<xsd:all>
|
||||
<xsd:element name="Records" type="xsd:integer"/>
|
||||
<xsd:element name="Tag" type="xsd:string"/>
|
||||
</xsd:all>
|
||||
</xsd:complexType>
|
||||
</xsd:element>
|
||||
<xsd:element name="LunListInfoIterEnd">
|
||||
<xsd:complexType>
|
||||
<xsd:all>
|
||||
<xsd:element name="Tag" type="xsd:string"/>
|
||||
</xsd:all>
|
||||
</xsd:complexType>
|
||||
</xsd:element>
|
||||
<xsd:element name="LunListInfoIterEndResult">
|
||||
<xsd:complexType/>
|
||||
</xsd:element>
|
||||
<xsd:element name="LunListInfoIterNext">
|
||||
<xsd:complexType>
|
||||
<xsd:all>
|
||||
<xsd:element name="Maximum" type="xsd:integer"/>
|
||||
<xsd:element name="Tag" type="xsd:string"/>
|
||||
</xsd:all>
|
||||
</xsd:complexType>
|
||||
</xsd:element>
|
||||
<xsd:element name="LunListInfoIterNextResult">
|
||||
<xsd:complexType>
|
||||
<xsd:all>
|
||||
<xsd:element name="Luns" type="na:ArrayOfLunInfo"/>
|
||||
<xsd:element name="Records" type="xsd:integer"/>
|
||||
</xsd:all>
|
||||
</xsd:complexType>
|
||||
</xsd:element>
|
||||
<xsd:element name="LunListInfoIterStart">
|
||||
<xsd:complexType>
|
||||
<xsd:all>
|
||||
<xsd:element minOccurs="0" name="ObjectNameOrId"
|
||||
type="na:ObjNameOrId"/>
|
||||
</xsd:all>
|
||||
</xsd:complexType>
|
||||
</xsd:element>
|
||||
<xsd:element name="LunListInfoIterStartResult">
|
||||
<xsd:complexType>
|
||||
<xsd:all>
|
||||
<xsd:element name="Records" type="xsd:integer"/>
|
||||
<xsd:element name="Tag" type="xsd:string"/>
|
||||
</xsd:all>
|
||||
</xsd:complexType>
|
||||
</xsd:element>
|
||||
<xsd:element name="StorageServiceDatasetProvision">
|
||||
<xsd:complexType>
|
||||
<xsd:all>
|
||||
<xsd:element minOccurs="0" name="AssumeConfirmation"
|
||||
type="xsd:boolean"/>
|
||||
<xsd:element name="DatasetName" type="na:ObjName"/>
|
||||
<xsd:element name="StorageServiceNameOrId" type="na:ObjNameOrId"/>
|
||||
<xsd:element minOccurs="0" name="StorageSetDetails"
|
||||
type="na:ArrayOfStorageSetInfo"/>
|
||||
</xsd:all>
|
||||
</xsd:complexType>
|
||||
</xsd:element>
|
||||
<xsd:element name="StorageServiceDatasetProvisionResult">
|
||||
<xsd:complexType>
|
||||
<xsd:all>
|
||||
<xsd:element minOccurs="0" name="ConformanceAlerts"
|
||||
type="na:ArrayOfConformanceAlert"/>
|
||||
<xsd:element name="DatasetId" type="na:ObjId"/>
|
||||
<xsd:element minOccurs="0" name="DryRunResults"
|
||||
type="na:ArrayOfDryRunResult"/>
|
||||
</xsd:all>
|
||||
</xsd:complexType>
|
||||
</xsd:element>
|
||||
<xsd:complexType name="ArrayOfDatasetInfo">
|
||||
<xsd:sequence>
|
||||
<xsd:element maxOccurs="unbounded" name="DatasetInfo"
|
||||
type="na:DatasetInfo"/>
|
||||
</xsd:sequence>
|
||||
</xsd:complexType>
|
||||
<xsd:complexType name="ArrayOfDatasetMemberInfo">
|
||||
<xsd:sequence>
|
||||
<xsd:element maxOccurs="unbounded" name="DatasetMemberInfo"
|
||||
type="na:DatasetMemberInfo"/>
|
||||
</xsd:sequence>
|
||||
</xsd:complexType>
|
||||
<xsd:complexType name="ArrayOfDatasetMemberParameter">
|
||||
<xsd:sequence>
|
||||
<xsd:element maxOccurs="unbounded" name="DatasetMemberParameter"
|
||||
type="na:DatasetMemberParameter"/>
|
||||
</xsd:sequence>
|
||||
</xsd:complexType>
|
||||
<xsd:complexType name="ArrayOfDpJobProgressEventInfo">
|
||||
<xsd:sequence>
|
||||
<xsd:element maxOccurs="unbounded" name="DpJobProgressEventInfo"
|
||||
type="na:DpJobProgressEventInfo"/>
|
||||
</xsd:sequence>
|
||||
</xsd:complexType>
|
||||
<xsd:complexType name="ArrayOfHostInfo">
|
||||
<xsd:sequence>
|
||||
<xsd:element maxOccurs="unbounded" name="HostInfo" type="na:HostInfo"/>
|
||||
</xsd:sequence>
|
||||
</xsd:complexType>
|
||||
<xsd:complexType name="ArrayOfJobInfo">
|
||||
<xsd:sequence>
|
||||
<xsd:element maxOccurs="unbounded" name="JobInfo" type="na:JobInfo"/>
|
||||
</xsd:sequence>
|
||||
</xsd:complexType>
|
||||
<xsd:complexType name="ArrayOfLunInfo">
|
||||
<xsd:sequence>
|
||||
<xsd:element maxOccurs="unbounded" name="LunInfo" type="na:LunInfo"/>
|
||||
</xsd:sequence>
|
||||
</xsd:complexType>
|
||||
<xsd:complexType name="ArrayOfStorageSetInfo">
|
||||
<xsd:sequence>
|
||||
<xsd:element maxOccurs="unbounded" name="StorageSetInfo"
|
||||
type="na:StorageSetInfo"/>
|
||||
</xsd:sequence>
|
||||
</xsd:complexType>
|
||||
<xsd:complexType name="DatasetExportInfo">
|
||||
<xsd:all>
|
||||
<xsd:element minOccurs="0" name="DatasetExportProtocol"
|
||||
type="na:DatasetExportProtocol"/>
|
||||
<xsd:element minOccurs="0" name="DatasetLunMappingInfo"
|
||||
type="na:DatasetLunMappingInfo"/>
|
||||
</xsd:all>
|
||||
</xsd:complexType>
|
||||
<xsd:simpleType name="DatasetExportProtocol">
|
||||
<xsd:restriction base="xsd:string"/>
|
||||
</xsd:simpleType>
|
||||
<xsd:complexType name="DatasetInfo">
|
||||
<xsd:all>
|
||||
<xsd:element name="DatasetId" type="na:ObjId"/>
|
||||
</xsd:all>
|
||||
</xsd:complexType>
|
||||
<xsd:complexType name="DatasetLunMappingInfo">
|
||||
<xsd:all>
|
||||
<xsd:element name="IgroupOsType" type="xsd:string"/>
|
||||
</xsd:all>
|
||||
</xsd:complexType>
|
||||
<xsd:complexType name="DatasetMemberInfo">
|
||||
<xsd:all>
|
||||
<xsd:element name="MemberId" type="na:ObjId"/>
|
||||
<xsd:element name="MemberName" type="na:ObjName"/>
|
||||
</xsd:all>
|
||||
</xsd:complexType>
|
||||
<xsd:complexType name="DatasetMemberParameter">
|
||||
<xsd:all>
|
||||
<xsd:element name="ObjectNameOrId" type="na:ObjNameOrId"/>
|
||||
</xsd:all>
|
||||
</xsd:complexType>
|
||||
<xsd:complexType name="DpJobProgressEventInfo">
|
||||
<xsd:all>
|
||||
<xsd:element name="EventStatus" type="na:ObjStatus"/>
|
||||
<xsd:element name="EventType" type="xsd:string"/>
|
||||
<xsd:element minOccurs="0" name="ProgressLunInfo"
|
||||
type="na:ProgressLunInfo"/>
|
||||
</xsd:all>
|
||||
</xsd:complexType>
|
||||
<xsd:simpleType name="DpPolicyNodeName">
|
||||
<xsd:restriction base="xsd:string"/>
|
||||
</xsd:simpleType>
|
||||
<xsd:simpleType name="HostId">
|
||||
<xsd:restriction base="xsd:integer"/>
|
||||
</xsd:simpleType>
|
||||
<xsd:complexType name="HostInfo">
|
||||
<xsd:all>
|
||||
<xsd:element name="HostAddress" type="xsd:string"/>
|
||||
<xsd:element name="HostId" type="na:HostId"/>
|
||||
<xsd:element name="HostName" type="xsd:string"/>
|
||||
</xsd:all>
|
||||
</xsd:complexType>
|
||||
<xsd:complexType name="JobInfo">
|
||||
<xsd:all>
|
||||
<xsd:element name="JobId" type="xsd:integer"/>
|
||||
</xsd:all>
|
||||
</xsd:complexType>
|
||||
<xsd:complexType name="LunInfo">
|
||||
<xsd:all>
|
||||
<xsd:element name="HostId" type="na:ObjId"/>
|
||||
<xsd:element name="LunPath" type="na:ObjName"/>
|
||||
</xsd:all>
|
||||
</xsd:complexType>
|
||||
<xsd:simpleType name="ObjId">
|
||||
<xsd:restriction base="xsd:integer"/>
|
||||
</xsd:simpleType>
|
||||
<xsd:simpleType name="ObjName">
|
||||
<xsd:restriction base="xsd:string"/>
|
||||
</xsd:simpleType>
|
||||
<xsd:simpleType name="ObjNameOrId">
|
||||
<xsd:restriction base="xsd:string"/>
|
||||
</xsd:simpleType>
|
||||
<xsd:simpleType name="ObjStatus">
|
||||
<xsd:restriction base="xsd:string"/>
|
||||
</xsd:simpleType>
|
||||
<xsd:complexType name="ProgressLunInfo">
|
||||
<xsd:all>
|
||||
<xsd:element name="LunPathId" type="na:ObjId"/>
|
||||
</xsd:all>
|
||||
</xsd:complexType>
|
||||
<xsd:complexType name="ProvisionMemberRequestInfo">
|
||||
<xsd:all>
|
||||
<xsd:element minOccurs="0" name="Description" type="xsd:string"/>
|
||||
<xsd:element minOccurs="0" name="MaximumSnapshotSpace"
|
||||
type="xsd:integer"/>
|
||||
<xsd:element name="Name" type="xsd:string"/>
|
||||
<xsd:element name="Size" type="xsd:integer"/>
|
||||
</xsd:all>
|
||||
</xsd:complexType>
|
||||
<xsd:complexType name="Request">
|
||||
<xsd:all>
|
||||
<xsd:element minOccurs="0" name="Args">
|
||||
<xsd:complexType>
|
||||
<xsd:sequence>
|
||||
<xsd:any maxOccurs="unbounded" minOccurs="0"/>
|
||||
</xsd:sequence>
|
||||
</xsd:complexType>
|
||||
</xsd:element>
|
||||
<xsd:element name="Name" type="xsd:string">
|
||||
</xsd:element>
|
||||
</xsd:all>
|
||||
</xsd:complexType>
|
||||
<xsd:complexType name="Response">
|
||||
<xsd:all>
|
||||
<xsd:element minOccurs="0" name="Errno" type="xsd:integer"/>
|
||||
<xsd:element minOccurs="0" name="Reason" type="xsd:string"/>
|
||||
<xsd:element minOccurs="0" name="Results">
|
||||
<xsd:complexType>
|
||||
<xsd:sequence>
|
||||
<xsd:any maxOccurs="unbounded" minOccurs="0"/>
|
||||
</xsd:sequence>
|
||||
</xsd:complexType>
|
||||
</xsd:element>
|
||||
<xsd:element name="Status" type="xsd:string"/>
|
||||
</xsd:all>
|
||||
</xsd:complexType>
|
||||
<xsd:complexType name="StorageSetInfo">
|
||||
<xsd:all>
|
||||
<xsd:element minOccurs="0" name="DatasetExportInfo"
|
||||
type="na:DatasetExportInfo"/>
|
||||
<xsd:element minOccurs="0" name="DpNodeName"
|
||||
type="na:DpPolicyNodeName"/>
|
||||
<xsd:element minOccurs="0" name="ServerNameOrId"
|
||||
type="na:ObjNameOrId"/>
|
||||
</xsd:all>
|
||||
</xsd:complexType>
|
||||
</xsd:schema></types>"""
|
||||
|
||||
WSDL_TRAILER = """<service name="DfmService">
|
||||
<port binding="na:DfmBinding" name="DfmPort">
|
||||
<soap:address location="https://HOST_NAME:8488/apis/soap/v1"/>
|
||||
</port></service></definitions>"""
|
||||
|
||||
RESPONSE_PREFIX = """<?xml version="1.0" encoding="UTF-8"?>
|
||||
<env:Envelope xmlns:env="http://schemas.xmlsoap.org/soap/envelope/"
|
||||
xmlns:na="http://www.netapp.com/management/v1"><env:Header/><env:Body>"""
|
||||
|
||||
RESPONSE_SUFFIX = """</env:Body></env:Envelope>"""
|
||||
|
||||
APIS = ['ApiProxy', 'DatasetListInfoIterStart', 'DatasetListInfoIterNext',
|
||||
'DatasetListInfoIterEnd', 'DatasetEditBegin', 'DatasetEditCommit',
|
||||
'DatasetProvisionMember', 'DatasetRemoveMember', 'DfmAbout',
|
||||
'DpJobProgressEventListIterStart', 'DpJobProgressEventListIterNext',
|
||||
'DpJobProgressEventListIterEnd', 'DatasetMemberListInfoIterStart',
|
||||
'DatasetMemberListInfoIterNext', 'DatasetMemberListInfoIterEnd',
|
||||
'HostListInfoIterStart', 'HostListInfoIterNext', 'HostListInfoIterEnd',
|
||||
'LunListInfoIterStart', 'LunListInfoIterNext', 'LunListInfoIterEnd',
|
||||
'StorageServiceDatasetProvision']
|
||||
|
||||
iter_count = 0
|
||||
iter_table = {}
|
||||
|
||||
|
||||
class FakeDfmServerHandler(BaseHTTPServer.BaseHTTPRequestHandler):
|
||||
"""HTTP handler that fakes enough stuff to allow the driver to run"""
|
||||
|
||||
def do_GET(s):
|
||||
"""Respond to a GET request."""
|
||||
if '/dfm.wsdl' != s.path:
|
||||
s.send_response(404)
|
||||
s.end_headers
|
||||
return
|
||||
s.send_response(200)
|
||||
s.send_header("Content-Type", "application/wsdl+xml")
|
||||
s.end_headers()
|
||||
out = s.wfile
|
||||
out.write(WSDL_HEADER)
|
||||
out.write(WSDL_TYPES)
|
||||
for api in APIS:
|
||||
out.write('<message name="%sRequest">' % api)
|
||||
out.write('<part element="na:%s" name="parameters"/>' % api)
|
||||
out.write('</message>')
|
||||
out.write('<message name="%sResponse">' % api)
|
||||
out.write('<part element="na:%sResult" name="results"/>' % api)
|
||||
out.write('</message>')
|
||||
out.write('<portType name="DfmInterface">')
|
||||
for api in APIS:
|
||||
out.write('<operation name="%s">' % api)
|
||||
out.write('<input message="na:%sRequest"/>' % api)
|
||||
out.write('<output message="na:%sResponse"/>' % api)
|
||||
out.write('</operation>')
|
||||
out.write('</portType>')
|
||||
out.write('<binding name="DfmBinding" type="na:DfmInterface">')
|
||||
out.write('<soap:binding style="document" ' +
|
||||
'transport="http://schemas.xmlsoap.org/soap/http"/>')
|
||||
for api in APIS:
|
||||
out.write('<operation name="%s">' % api)
|
||||
out.write('<soap:operation soapAction="urn:%s"/>' % api)
|
||||
out.write('<input><soap:body use="literal"/></input>')
|
||||
out.write('<output><soap:body use="literal"/></output>')
|
||||
out.write('</operation>')
|
||||
out.write('</binding>')
|
||||
out.write(WSDL_TRAILER)
|
||||
return
|
||||
|
||||
def do_POST(s):
|
||||
"""Respond to a POST request."""
|
||||
if '/apis/soap/v1' != s.path:
|
||||
s.send_response(404)
|
||||
s.end_headers
|
||||
return
|
||||
request_xml = s.rfile.read(int(s.headers['Content-Length']))
|
||||
ntap_ns = 'http://www.netapp.com/management/v1'
|
||||
nsmap = {'env': 'http://schemas.xmlsoap.org/soap/envelope/',
|
||||
'na': ntap_ns}
|
||||
root = etree.fromstring(request_xml)
|
||||
|
||||
body = root.xpath('/env:Envelope/env:Body', namespaces=nsmap)[0]
|
||||
request = body.getchildren()[0]
|
||||
tag = request.tag
|
||||
if not tag.startswith('{' + ntap_ns + '}'):
|
||||
s.send_response(500)
|
||||
s.end_headers
|
||||
return
|
||||
api = tag[(2 + len(ntap_ns)):]
|
||||
global iter_count
|
||||
global iter_table
|
||||
if 'DatasetListInfoIterStart' == api:
|
||||
body = """<na:DatasetListInfoIterStartResult>
|
||||
<na:Records>1</na:Records>
|
||||
<na:Tag>dataset</na:Tag>
|
||||
</na:DatasetListInfoIterStartResult>"""
|
||||
elif 'DatasetListInfoIterNext' == api:
|
||||
body = """<na:DatasetListInfoIterNextResult>
|
||||
<na:Datasets>
|
||||
<na:DatasetInfo>
|
||||
<na:DatasetId>0</na:DatasetId>
|
||||
</na:DatasetInfo>
|
||||
</na:Datasets>
|
||||
<na:Records>1</na:Records>
|
||||
</na:DatasetListInfoIterNextResult>"""
|
||||
elif 'DatasetListInfoIterEnd' == api:
|
||||
body = """<na:DatasetListInfoIterEndResult/>"""
|
||||
elif 'DatasetEditBegin' == api:
|
||||
body = """<na:DatasetEditBeginResult>
|
||||
<na:EditLockId>0</na:EditLockId>
|
||||
</na:DatasetEditBeginResult>"""
|
||||
elif 'DatasetEditCommit' == api:
|
||||
body = """<na:DatasetEditCommitResult>
|
||||
<na:IsProvisioningFailure>false</na:IsProvisioningFailure>
|
||||
<na:JobIds>
|
||||
<na:JobInfo>
|
||||
<na:JobId>0</na:JobId>
|
||||
</na:JobInfo>
|
||||
</na:JobIds>
|
||||
</na:DatasetEditCommitResult>"""
|
||||
elif 'DatasetProvisionMember' == api:
|
||||
body = """<na:DatasetProvisionMemberResult/>"""
|
||||
elif 'DatasetRemoveMember' == api:
|
||||
body = """<na:DatasetRemoveMemberResult/>"""
|
||||
elif 'DfmAbout' == api:
|
||||
body = """<na:DfmAboutResult/>"""
|
||||
elif 'DpJobProgressEventListIterStart' == api:
|
||||
iter_name = 'dpjobprogress_%s' % iter_count
|
||||
iter_count = iter_count + 1
|
||||
iter_table[iter_name] = 0
|
||||
body = """<na:DpJobProgressEventListIterStartResult>
|
||||
<na:Records>2</na:Records>
|
||||
<na:Tag>%s</na:Tag>
|
||||
</na:DpJobProgressEventListIterStartResult>""" % iter_name
|
||||
elif 'DpJobProgressEventListIterNext' == api:
|
||||
tags = body.xpath('na:DpJobProgressEventListIterNext/na:Tag',
|
||||
namespaces=nsmap)
|
||||
iter_name = tags[0].text
|
||||
if iter_table[iter_name]:
|
||||
body = """<na:DpJobProgressEventListIterNextResult/>"""
|
||||
else:
|
||||
iter_table[iter_name] = 1
|
||||
body = """<na:DpJobProgressEventListIterNextResult>
|
||||
<na:ProgressEvents>
|
||||
<na:DpJobProgressEventInfo>
|
||||
<na:EventStatus>normal</na:EventStatus>
|
||||
<na:EventType>lun-create</na:EventType>
|
||||
<na:ProgressLunInfo>
|
||||
<na:LunPathId>0</na:LunPathId>
|
||||
</na:ProgressLunInfo>
|
||||
</na:DpJobProgressEventInfo>
|
||||
<na:DpJobProgressEventInfo>
|
||||
<na:EventStatus>normal</na:EventStatus>
|
||||
<na:EventType>job-end</na:EventType>
|
||||
</na:DpJobProgressEventInfo>
|
||||
</na:ProgressEvents>
|
||||
<na:Records>2</na:Records>
|
||||
</na:DpJobProgressEventListIterNextResult>"""
|
||||
elif 'DpJobProgressEventListIterEnd' == api:
|
||||
body = """<na:DpJobProgressEventListIterEndResult/>"""
|
||||
elif 'DatasetMemberListInfoIterStart' == api:
|
||||
body = """<na:DatasetMemberListInfoIterStartResult>
|
||||
<na:Records>1</na:Records>
|
||||
<na:Tag>dataset-member</na:Tag>
|
||||
</na:DatasetMemberListInfoIterStartResult>"""
|
||||
elif 'DatasetMemberListInfoIterNext' == api:
|
||||
name = 'filer:/OpenStack_testproj/volume-00000001/volume-00000001'
|
||||
body = """<na:DatasetMemberListInfoIterNextResult>
|
||||
<na:DatasetMembers>
|
||||
<na:DatasetMemberInfo>
|
||||
<na:MemberId>0</na:MemberId>
|
||||
<na:MemberName>%s</na:MemberName>
|
||||
</na:DatasetMemberInfo>
|
||||
</na:DatasetMembers>
|
||||
<na:Records>1</na:Records>
|
||||
</na:DatasetMemberListInfoIterNextResult>""" % name
|
||||
elif 'DatasetMemberListInfoIterEnd' == api:
|
||||
body = """<na:DatasetMemberListInfoIterEndResult/>"""
|
||||
elif 'HostListInfoIterStart' == api:
|
||||
body = """<na:HostListInfoIterStartResult>
|
||||
<na:Records>1</na:Records>
|
||||
<na:Tag>host</na:Tag>
|
||||
</na:HostListInfoIterStartResult>"""
|
||||
elif 'HostListInfoIterNext' == api:
|
||||
body = """<na:HostListInfoIterNextResult>
|
||||
<na:Hosts>
|
||||
<na:HostInfo>
|
||||
<na:HostAddress>1.2.3.4</na:HostAddress>
|
||||
<na:HostId>0</na:HostId>
|
||||
<na:HostName>filer</na:HostName>
|
||||
</na:HostInfo>
|
||||
</na:Hosts>
|
||||
<na:Records>1</na:Records>
|
||||
</na:HostListInfoIterNextResult>"""
|
||||
elif 'HostListInfoIterEnd' == api:
|
||||
body = """<na:HostListInfoIterEndResult/>"""
|
||||
elif 'LunListInfoIterStart' == api:
|
||||
body = """<na:LunListInfoIterStartResult>
|
||||
<na:Records>1</na:Records>
|
||||
<na:Tag>lun</na:Tag>
|
||||
</na:LunListInfoIterStartResult>"""
|
||||
elif 'LunListInfoIterNext' == api:
|
||||
path = 'OpenStack_testproj/volume-00000001/volume-00000001'
|
||||
body = """<na:LunListInfoIterNextResult>
|
||||
<na:Luns>
|
||||
<na:LunInfo>
|
||||
<na:HostId>0</na:HostId>
|
||||
<na:LunPath>%s</na:LunPath>
|
||||
</na:LunInfo>
|
||||
</na:Luns>
|
||||
<na:Records>1</na:Records>
|
||||
</na:LunListInfoIterNextResult>""" % path
|
||||
elif 'LunListInfoIterEnd' == api:
|
||||
body = """<na:LunListInfoIterEndResult/>"""
|
||||
elif 'ApiProxy' == api:
|
||||
names = body.xpath('na:ApiProxy/na:Request/na:Name',
|
||||
namespaces=nsmap)
|
||||
proxy = names[0].text
|
||||
if 'igroup-list-info' == proxy:
|
||||
igroup = 'openstack-iqn.1993-08.org.debian:01:23456789'
|
||||
initiator = 'iqn.1993-08.org.debian:01:23456789'
|
||||
proxy_body = """<initiator-groups>
|
||||
<initiator-group-info>
|
||||
<initiator-group-name>%s</initiator-group-name>
|
||||
<initiator-group-type>iscsi</initiator-group-type>
|
||||
<initiator-group-os-type>linux</initiator-group-os-type>
|
||||
<initiators>
|
||||
<initiator-info>
|
||||
<initiator-name>%s</initiator-name>
|
||||
</initiator-info>
|
||||
</initiators>
|
||||
</initiator-group-info>
|
||||
</initiator-groups>""" % (igroup, initiator)
|
||||
elif 'igroup-create' == proxy:
|
||||
proxy_body = ''
|
||||
elif 'igroup-add' == proxy:
|
||||
proxy_body = ''
|
||||
elif 'lun-map-list-info' == proxy:
|
||||
proxy_body = '<initiator-groups/>'
|
||||
elif 'lun-map' == proxy:
|
||||
proxy_body = '<lun-id-assigned>0</lun-id-assigned>'
|
||||
elif 'lun-unmap' == proxy:
|
||||
proxy_body = ''
|
||||
elif 'iscsi-portal-list-info' == proxy:
|
||||
proxy_body = """<iscsi-portal-list-entries>
|
||||
<iscsi-portal-list-entry-info>
|
||||
<ip-address>1.2.3.4</ip-address>
|
||||
<ip-port>3260</ip-port>
|
||||
<tpgroup-tag>1000</tpgroup-tag>
|
||||
</iscsi-portal-list-entry-info>
|
||||
</iscsi-portal-list-entries>"""
|
||||
elif 'iscsi-node-get-name' == proxy:
|
||||
target = 'iqn.1992-08.com.netapp:sn.111111111'
|
||||
proxy_body = '<node-name>%s</node-name>' % target
|
||||
else:
|
||||
# Unknown proxy API
|
||||
s.send_response(500)
|
||||
s.end_headers
|
||||
return
|
||||
api = api + ':' + proxy
|
||||
proxy_header = '<na:ApiProxyResult><na:Response><na:Results>'
|
||||
proxy_trailer = """</na:Results><na:Status>passed</na:Status>
|
||||
</na:Response></na:ApiProxyResult>"""
|
||||
body = proxy_header + proxy_body + proxy_trailer
|
||||
else:
|
||||
# Unknown API
|
||||
s.send_response(500)
|
||||
s.end_headers
|
||||
return
|
||||
s.send_response(200)
|
||||
s.send_header("Content-Type", "text/xml; charset=utf-8")
|
||||
s.end_headers()
|
||||
s.wfile.write(RESPONSE_PREFIX)
|
||||
s.wfile.write(body)
|
||||
s.wfile.write(RESPONSE_SUFFIX)
|
||||
return
|
||||
|
||||
|
||||
class FakeHttplibSocket(object):
|
||||
"""A fake socket implementation for httplib.HTTPResponse"""
|
||||
def __init__(self, value):
|
||||
self._rbuffer = StringIO.StringIO(value)
|
||||
self._wbuffer = StringIO.StringIO('')
|
||||
oldclose = self._wbuffer.close
|
||||
|
||||
def newclose():
|
||||
self.result = self._wbuffer.getvalue()
|
||||
oldclose()
|
||||
self._wbuffer.close = newclose
|
||||
|
||||
def makefile(self, mode, _other):
|
||||
"""Returns the socket's internal buffer"""
|
||||
if mode == 'r' or mode == 'rb':
|
||||
return self._rbuffer
|
||||
if mode == 'w' or mode == 'wb':
|
||||
return self._wbuffer
|
||||
|
||||
|
||||
class FakeHTTPConnection(object):
|
||||
"""A fake httplib.HTTPConnection for netapp tests
|
||||
|
||||
Requests made via this connection actually get translated and routed into
|
||||
the fake Dfm handler above, we then turn the response into
|
||||
the httplib.HTTPResponse that the caller expects.
|
||||
"""
|
||||
def __init__(self, host, timeout=None):
|
||||
self.host = host
|
||||
|
||||
def request(self, method, path, data=None, headers=None):
|
||||
if not headers:
|
||||
headers = {}
|
||||
req_str = '%s %s HTTP/1.1\r\n' % (method, path)
|
||||
for key, value in headers.iteritems():
|
||||
req_str += "%s: %s\r\n" % (key, value)
|
||||
if data:
|
||||
req_str += '\r\n%s' % data
|
||||
|
||||
# NOTE(vish): normally the http transport normailizes from unicode
|
||||
sock = FakeHttplibSocket(req_str.decode("latin-1").encode("utf-8"))
|
||||
# NOTE(vish): stop the server from trying to look up address from
|
||||
# the fake socket
|
||||
FakeDfmServerHandler.address_string = lambda x: '127.0.0.1'
|
||||
self.app = FakeDfmServerHandler(sock, '127.0.0.1:8088', None)
|
||||
|
||||
self.sock = FakeHttplibSocket(sock.result)
|
||||
self.http_response = httplib.HTTPResponse(self.sock)
|
||||
|
||||
def set_debuglevel(self, level):
|
||||
pass
|
||||
|
||||
def getresponse(self):
|
||||
self.http_response.begin()
|
||||
return self.http_response
|
||||
|
||||
def getresponsebody(self):
|
||||
return self.sock.result
|
||||
|
||||
|
||||
class NetAppDriverTestCase(test.TestCase):
|
||||
"""Test case for NetAppISCSIDriver"""
|
||||
STORAGE_SERVICE = 'Thin Provisioned Space for VMFS Datastores'
|
||||
PROJECT_ID = 'testproj'
|
||||
VOLUME_NAME = 'volume-00000001'
|
||||
VOLUME_SIZE = 2147483648L # 2 GB
|
||||
INITIATOR = 'iqn.1993-08.org.debian:01:23456789'
|
||||
|
||||
def setUp(self):
|
||||
super(NetAppDriverTestCase, self).setUp()
|
||||
driver = NetAppISCSIDriver()
|
||||
self.stubs.Set(httplib, 'HTTPConnection', FakeHTTPConnection)
|
||||
driver._create_client('http://localhost:8088/dfm.wsdl',
|
||||
'root', 'password', 'localhost', 8088)
|
||||
driver._set_storage_service(self.STORAGE_SERVICE)
|
||||
self.driver = driver
|
||||
|
||||
def test_connect(self):
|
||||
self.driver.check_for_setup_error()
|
||||
|
||||
def test_create_destroy(self):
|
||||
self.driver._provision(self.VOLUME_NAME, None, self.PROJECT_ID,
|
||||
self.VOLUME_SIZE)
|
||||
self.driver._remove_destroy(self.VOLUME_NAME, self.PROJECT_ID)
|
||||
|
||||
def test_map_unmap(self):
|
||||
self.driver._provision(self.VOLUME_NAME, None, self.PROJECT_ID,
|
||||
self.VOLUME_SIZE)
|
||||
volume = {'name': self.VOLUME_NAME, 'project_id': self.PROJECT_ID,
|
||||
'id': 0, 'provider_auth': None}
|
||||
updates = self.driver._get_export(volume)
|
||||
self.assertTrue(updates['provider_location'])
|
||||
volume['provider_location'] = updates['provider_location']
|
||||
connector = {'initiator': self.INITIATOR}
|
||||
connection_info = self.driver.initialize_connection(volume, connector)
|
||||
self.assertEqual(connection_info['driver_volume_type'], 'iscsi')
|
||||
properties = connection_info['data']
|
||||
self.driver.terminate_connection(volume, connector)
|
||||
self.driver._remove_destroy(self.VOLUME_NAME, self.PROJECT_ID)
|
676
nova/volume/netapp.py
Normal file
676
nova/volume/netapp.py
Normal file
@ -0,0 +1,676 @@
|
||||
# vim: tabstop=4 shiftwidth=4 softtabstop=4
|
||||
|
||||
# Copyright (c) 2012 NetApp, Inc.
|
||||
# Copyright (c) 2012 OpenStack LLC.
|
||||
# All Rights Reserved.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License"); you may
|
||||
# not use this file except in compliance with the License. You may obtain
|
||||
# a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
# License for the specific language governing permissions and limitations
|
||||
# under the License.
|
||||
"""
|
||||
Volume driver for NetApp storage systems.
|
||||
|
||||
This driver requires NetApp OnCommand 5.0 and one or more Data
|
||||
ONTAP 7-mode storage systems with installed iSCSI licenses.
|
||||
|
||||
"""
|
||||
|
||||
import time
|
||||
import string
|
||||
|
||||
import suds
|
||||
from suds import client
|
||||
from suds.sax import text
|
||||
|
||||
from nova import exception
|
||||
from nova import flags
|
||||
from nova import log as logging
|
||||
from nova.openstack.common import cfg
|
||||
from nova.volume import driver
|
||||
|
||||
LOG = logging.getLogger("nova.volume.driver")
|
||||
|
||||
netapp_opts = [
|
||||
cfg.StrOpt('netapp_wsdl_url',
|
||||
default=None,
|
||||
help='URL of the WSDL file for the DFM server'),
|
||||
cfg.StrOpt('netapp_login',
|
||||
default=None,
|
||||
help='User name for the DFM server'),
|
||||
cfg.StrOpt('netapp_password',
|
||||
default=None,
|
||||
help='Password for the DFM server'),
|
||||
cfg.StrOpt('netapp_server_hostname',
|
||||
default=None,
|
||||
help='Hostname for the DFM server'),
|
||||
cfg.IntOpt('netapp_server_port',
|
||||
default=8088,
|
||||
help='Port number for the DFM server'),
|
||||
cfg.StrOpt('netapp_storage_service',
|
||||
default=None,
|
||||
help='Storage service to use for provisioning'),
|
||||
cfg.StrOpt('netapp_vfiler',
|
||||
default=None,
|
||||
help='Vfiler to use for provisioning'),
|
||||
]
|
||||
|
||||
FLAGS = flags.FLAGS
|
||||
FLAGS.register_opts(netapp_opts)
|
||||
|
||||
|
||||
class NetAppISCSIDriver(driver.ISCSIDriver):
|
||||
"""NetApp iSCSI volume driver."""
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super(NetAppISCSIDriver, self).__init__(*args, **kwargs)
|
||||
|
||||
def _check_fail(self, request, response):
|
||||
if 'failed' == response.Status:
|
||||
name = request.Name
|
||||
reason = response.Reason
|
||||
msg = _('API %(name)sfailed: %(reason)s')
|
||||
raise exception.Error(msg % locals())
|
||||
|
||||
def _create_client(self, wsdl_url, login, password, hostname, port):
|
||||
"""
|
||||
Instantiate a "suds" client to make web services calls to the
|
||||
DFM server. Note that the WSDL file is quite large and may take
|
||||
a few seconds to parse.
|
||||
"""
|
||||
self.client = client.Client(wsdl_url,
|
||||
username=login,
|
||||
password=password)
|
||||
soap_url = 'http://%s:%s/apis/soap/v1' % (hostname, port)
|
||||
self.client.set_options(location=soap_url)
|
||||
|
||||
def _set_storage_service(self, storage_service):
|
||||
"""Set the storage service to use for provisioning"""
|
||||
self.storage_service = storage_service
|
||||
|
||||
def _set_vfiler(self, vfiler):
|
||||
"""Set the vfiler to use for provisioning"""
|
||||
self.vfiler = vfiler
|
||||
|
||||
def _check_flags(self):
|
||||
"""Ensure that the flags we care about are set."""
|
||||
required_flags = ['netapp_wsdl_url', 'netapp_login', 'netapp_password',
|
||||
'netapp_server_hostname', 'netapp_server_port',
|
||||
'netapp_storage_service']
|
||||
for flag in required_flags:
|
||||
if not getattr(FLAGS, flag, None):
|
||||
raise exception.Error(_('%s is not set') % flag)
|
||||
|
||||
def do_setup(self, context):
|
||||
"""
|
||||
Called one time by the manager after the driver is loaded.
|
||||
Validate the flags we care about and setup the suds (web services)
|
||||
client.
|
||||
"""
|
||||
self._check_flags()
|
||||
self._create_client(FLAGS.netapp_wsdl_url, FLAGS.netapp_login,
|
||||
FLAGS.netapp_password, FLAGS.netapp_server_hostname,
|
||||
FLAGS.netapp_server_port)
|
||||
self._set_storage_service(FLAGS.netapp_storage_service)
|
||||
if FLAGS.netapp_vfiler:
|
||||
self._set_vfiler(FLAGS.netapp_vfiler)
|
||||
|
||||
def check_for_setup_error(self):
|
||||
"""Invoke a web services API to make sure we can talk to the server."""
|
||||
res = self.client.service.DfmAbout()
|
||||
LOG.debug(_("Connected to DFM server"))
|
||||
|
||||
def _get_job_progress(self, job_id):
|
||||
"""
|
||||
Obtain the latest progress report for the job and return the
|
||||
list of progress events.
|
||||
"""
|
||||
server = self.client.service
|
||||
res = server.DpJobProgressEventListIterStart(JobId=job_id)
|
||||
tag = res.Tag
|
||||
event_list = []
|
||||
try:
|
||||
while True:
|
||||
res = server.DpJobProgressEventListIterNext(Tag=tag,
|
||||
Maximum=100)
|
||||
if not hasattr(res, 'ProgressEvents'):
|
||||
break
|
||||
event_list += res.ProgressEvents.DpJobProgressEventInfo
|
||||
finally:
|
||||
server.DpJobProgressEventListIterEnd(Tag=tag)
|
||||
return event_list
|
||||
|
||||
def _wait_for_job(self, job_id):
|
||||
"""
|
||||
Poll the job until it completes or an error is detected. Return the
|
||||
final list of progress events if it completes successfully.
|
||||
"""
|
||||
while True:
|
||||
events = self._get_job_progress(job_id)
|
||||
for event in events:
|
||||
if event.EventStatus == 'error':
|
||||
raise exception.Error(_('Job failed: %s') %
|
||||
(event.ErrorMessage))
|
||||
if event.EventType == 'job-end':
|
||||
return events
|
||||
time.sleep(5)
|
||||
|
||||
def _dataset_name(self, project):
|
||||
"""Return the dataset name for a given project """
|
||||
_project = string.replace(string.replace(project, ' ', '_'), '-', '_')
|
||||
return 'OpenStack_' + _project
|
||||
|
||||
def _does_dataset_exist(self, dataset_name):
|
||||
"""Check if a dataset already exists"""
|
||||
server = self.client.service
|
||||
try:
|
||||
res = server.DatasetListInfoIterStart(ObjectNameOrId=dataset_name)
|
||||
tag = res.Tag
|
||||
except suds.WebFault:
|
||||
return False
|
||||
try:
|
||||
res = server.DatasetListInfoIterNext(Tag=tag, Maximum=1)
|
||||
if hasattr(res, 'Datasets') and res.Datasets.DatasetInfo:
|
||||
return True
|
||||
finally:
|
||||
server.DatasetListInfoIterEnd(Tag=tag)
|
||||
return False
|
||||
|
||||
def _create_dataset(self, dataset_name):
|
||||
"""
|
||||
Create a new dataset using the storage service. The export settings are
|
||||
set to create iSCSI LUNs aligned for Linux.
|
||||
"""
|
||||
server = self.client.service
|
||||
|
||||
lunmap = self.client.factory.create('DatasetLunMappingInfo')
|
||||
lunmap.IgroupOsType = 'linux'
|
||||
export = self.client.factory.create('DatasetExportInfo')
|
||||
export.DatasetExportProtocol = 'iscsi'
|
||||
export.DatasetLunMappingInfo = lunmap
|
||||
detail = self.client.factory.create('StorageSetInfo')
|
||||
detail.DpNodeName = 'Primary data'
|
||||
detail.DatasetExportInfo = export
|
||||
if hasattr(self, 'vfiler'):
|
||||
detail.ServerNameOrId = self.vfiler
|
||||
details = self.client.factory.create('ArrayOfStorageSetInfo')
|
||||
details.StorageSetInfo = [detail]
|
||||
|
||||
server.StorageServiceDatasetProvision(
|
||||
StorageServiceNameOrId=self.storage_service,
|
||||
DatasetName=dataset_name,
|
||||
AssumeConfirmation=True,
|
||||
StorageSetDetails=details)
|
||||
|
||||
def _provision(self, name, description, project, size):
|
||||
"""
|
||||
Provision a LUN through provisioning manager. The LUN will be created
|
||||
inside a dataset associated with the project. If the dataset doesn't
|
||||
already exist, we create it using the storage service specified in the
|
||||
nova conf.
|
||||
"""
|
||||
|
||||
dataset_name = self._dataset_name(project)
|
||||
if not self._does_dataset_exist(dataset_name):
|
||||
self._create_dataset(dataset_name)
|
||||
|
||||
info = self.client.factory.create('ProvisionMemberRequestInfo')
|
||||
info.Name = name
|
||||
if description:
|
||||
info.Description = description
|
||||
info.Size = size
|
||||
info.MaximumSnapshotSpace = 2 * long(size)
|
||||
|
||||
server = self.client.service
|
||||
lock_id = server.DatasetEditBegin(DatasetNameOrId=dataset_name)
|
||||
try:
|
||||
server.DatasetProvisionMember(EditLockId=lock_id,
|
||||
ProvisionMemberRequestInfo=info)
|
||||
res = server.DatasetEditCommit(EditLockId=lock_id,
|
||||
AssumeConfirmation=True)
|
||||
except (suds.WebFault, Exception):
|
||||
server.DatasetEditRollback(EditLockId=lock_id)
|
||||
raise exception.Error(_('Failed to provision dataset member'))
|
||||
|
||||
lun_id = None
|
||||
|
||||
for info in res.JobIds.JobInfo:
|
||||
events = self._wait_for_job(info.JobId)
|
||||
for event in events:
|
||||
if event.EventType != 'lun-create':
|
||||
continue
|
||||
lun_id = event.ProgressLunInfo.LunPathId
|
||||
|
||||
if not lun_id:
|
||||
raise exception.Error(_('No LUN was created by the provision job'))
|
||||
|
||||
def _remove_destroy(self, name, project):
|
||||
"""
|
||||
Remove the LUN from the dataset and destroy the actual LUN on the
|
||||
storage system.
|
||||
"""
|
||||
lun_id = self._get_lun_id(name, project)
|
||||
if not lun_id:
|
||||
raise exception.Error(_("Failed to find LUN ID for volume %s") %
|
||||
(name))
|
||||
|
||||
member = self.client.factory.create('DatasetMemberParameter')
|
||||
member.ObjectNameOrId = lun_id
|
||||
members = self.client.factory.create('ArrayOfDatasetMemberParameter')
|
||||
members.DatasetMemberParameter = [member]
|
||||
|
||||
dataset_name = self._dataset_name(project)
|
||||
|
||||
server = self.client.service
|
||||
lock_id = server.DatasetEditBegin(DatasetNameOrId=dataset_name)
|
||||
try:
|
||||
server.DatasetRemoveMember(EditLockId=lock_id, Destroy=True,
|
||||
DatasetMemberParameters=members)
|
||||
server.DatasetEditCommit(EditLockId=lock_id,
|
||||
AssumeConfirmation=True)
|
||||
except (suds.WebFault, Exception):
|
||||
server.DatasetEditRollback(EditLockId=lock_id)
|
||||
msg = _('Failed to remove and delete dataset member')
|
||||
raise exception.Error(msg)
|
||||
|
||||
def create_volume(self, volume):
|
||||
"""Driver entry point for creating a new volume"""
|
||||
default_size = '104857600' # 100 MB
|
||||
gigabytes = 1073741824L # 2^30
|
||||
name = volume['name']
|
||||
project = volume['project_id']
|
||||
display_name = volume['display_name']
|
||||
display_description = volume['display_description']
|
||||
if display_name:
|
||||
if display_description:
|
||||
description = display_name + "\n" + display_description
|
||||
else:
|
||||
description = display_name
|
||||
elif display_description:
|
||||
description = display_description
|
||||
if int(volume['size']) == 0:
|
||||
size = default_size
|
||||
else:
|
||||
size = str(int(volume['size']) * gigabytes)
|
||||
self._provision(name, description, project, size)
|
||||
|
||||
def delete_volume(self, volume):
|
||||
"""Driver entry point for destroying existing volumes"""
|
||||
name = volume['name']
|
||||
project = volume['project_id']
|
||||
self._remove_destroy(name, project)
|
||||
|
||||
def _get_lun_id(self, name, project):
|
||||
"""
|
||||
Given the name of a volume, find the DFM (OnCommand) ID of the LUN
|
||||
corresponding to that volume. Currently we do this by enumerating
|
||||
all of the LUNs in the dataset and matching the names against the
|
||||
OpenStack volume name.
|
||||
|
||||
This could become a performance bottleneck in very large installations
|
||||
in which case possible options for mitigating the problem are:
|
||||
1) Store the LUN ID alongside the volume in the nova DB (if possible)
|
||||
2) Cache the list of LUNs in the dataset in driver memory
|
||||
3) Store the volume to LUN ID mappings in a local file
|
||||
"""
|
||||
dataset_name = self._dataset_name(project)
|
||||
|
||||
server = self.client.service
|
||||
res = server.DatasetMemberListInfoIterStart(
|
||||
DatasetNameOrId=dataset_name,
|
||||
IncludeExportsInfo=True,
|
||||
IncludeIndirect=True,
|
||||
MemberType='lun_path')
|
||||
tag = res.Tag
|
||||
suffix = '/' + name
|
||||
try:
|
||||
while True:
|
||||
res = server.DatasetMemberListInfoIterNext(Tag=tag,
|
||||
Maximum=100)
|
||||
if (not hasattr(res, 'DatasetMembers') or
|
||||
not res.DatasetMembers):
|
||||
break
|
||||
for member in res.DatasetMembers.DatasetMemberInfo:
|
||||
if member.MemberName.endswith(suffix):
|
||||
return member.MemberId
|
||||
finally:
|
||||
server.DatasetMemberListInfoIterEnd(Tag=tag)
|
||||
|
||||
def _get_lun_details(self, lun_id):
|
||||
"""Given the ID of a LUN, get the details about that LUN"""
|
||||
server = self.client.service
|
||||
res = server.LunListInfoIterStart(ObjectNameOrId=lun_id)
|
||||
tag = res.Tag
|
||||
try:
|
||||
res = server.LunListInfoIterNext(Tag=tag, Maximum=1)
|
||||
if hasattr(res, 'Luns') and res.Luns.LunInfo:
|
||||
return res.Luns.LunInfo[0]
|
||||
finally:
|
||||
server.LunListInfoIterEnd(Tag=tag)
|
||||
|
||||
def _get_host_details(self, host_id):
|
||||
"""
|
||||
Given the ID of a host (storage system), get the details about that
|
||||
host.
|
||||
"""
|
||||
server = self.client.service
|
||||
res = server.HostListInfoIterStart(ObjectNameOrId=host_id)
|
||||
tag = res.Tag
|
||||
try:
|
||||
res = server.HostListInfoIterNext(Tag=tag, Maximum=1)
|
||||
if hasattr(res, 'Hosts') and res.Hosts.HostInfo:
|
||||
return res.Hosts.HostInfo[0]
|
||||
finally:
|
||||
server.HostListInfoIterEnd(Tag=tag)
|
||||
|
||||
def _get_iqn_for_host(self, host_id):
|
||||
"""Get the iSCSI Target Name for a storage system"""
|
||||
request = self.client.factory.create('Request')
|
||||
request.Name = 'iscsi-node-get-name'
|
||||
response = self.client.service.ApiProxy(Target=host_id,
|
||||
Request=request)
|
||||
self._check_fail(request, response)
|
||||
return response.Results['node-name'][0]
|
||||
|
||||
def _api_elem_is_empty(self, elem):
|
||||
"""
|
||||
Helper routine to figure out if a list returned from a proxy API
|
||||
is empty. This is necessary because the API proxy produces nasty
|
||||
looking XML.
|
||||
"""
|
||||
if not type(elem) is list:
|
||||
return True
|
||||
if 0 == len(elem):
|
||||
return True
|
||||
child = elem[0]
|
||||
if isinstance(child, text.Text):
|
||||
return True
|
||||
if type(child) is str:
|
||||
return True
|
||||
return False
|
||||
|
||||
def _get_target_portal_for_host(self, host_id, host_address):
|
||||
"""
|
||||
Get the iSCSI Target Portal details for a particular IP address
|
||||
on a storage system.
|
||||
"""
|
||||
request = self.client.factory.create('Request')
|
||||
request.Name = 'iscsi-portal-list-info'
|
||||
response = self.client.service.ApiProxy(Target=host_id,
|
||||
Request=request)
|
||||
self._check_fail(request, response)
|
||||
portal = {}
|
||||
portals = response.Results['iscsi-portal-list-entries']
|
||||
if self._api_elem_is_empty(portals):
|
||||
return portal
|
||||
portal_infos = portals[0]['iscsi-portal-list-entry-info']
|
||||
for portal_info in portal_infos:
|
||||
portal['address'] = portal_info['ip-address'][0]
|
||||
portal['port'] = portal_info['ip-port'][0]
|
||||
portal['portal'] = portal_info['tpgroup-tag'][0]
|
||||
if host_address == portal['address']:
|
||||
break
|
||||
return portal
|
||||
|
||||
def _get_export(self, volume):
|
||||
"""
|
||||
Looks up the LUN in DFM based on the volume and project name, then get
|
||||
the LUN's ID. We store that value in the database instead of the iSCSI
|
||||
details because we will not have the true iSCSI details until masking
|
||||
time (when initialize_connection() is called).
|
||||
"""
|
||||
name = volume['name']
|
||||
project = volume['project_id']
|
||||
lun_id = self._get_lun_id(name, project)
|
||||
if not lun_id:
|
||||
msg = _("Failed to find LUN ID for volume %s")
|
||||
raise exception.Error(msg % name)
|
||||
return {'provider_location': lun_id}
|
||||
|
||||
def ensure_export(self, context, volume):
|
||||
"""
|
||||
Driver entry point to get the iSCSI details about an existing volume
|
||||
"""
|
||||
return self._get_export(volume)
|
||||
|
||||
def create_export(self, context, volume):
|
||||
"""
|
||||
Driver entry point to get the iSCSI details about a new volume
|
||||
"""
|
||||
return self._get_export(volume)
|
||||
|
||||
def remove_export(self, context, volume):
|
||||
"""
|
||||
Since exporting is idempotent in this driver, we have nothing
|
||||
to do for unexporting.
|
||||
"""
|
||||
pass
|
||||
|
||||
def _find_igroup_for_initiator(self, host_id, initiator_name):
|
||||
"""
|
||||
Look for an existing igroup (initiator group) on the storage system
|
||||
containing a given iSCSI initiator and return the name of the igroup.
|
||||
"""
|
||||
request = self.client.factory.create('Request')
|
||||
request.Name = 'igroup-list-info'
|
||||
response = self.client.service.ApiProxy(Target=host_id,
|
||||
Request=request)
|
||||
self._check_fail(request, response)
|
||||
igroups = response.Results['initiator-groups']
|
||||
if self._api_elem_is_empty(igroups):
|
||||
return None
|
||||
igroup_infos = igroups[0]['initiator-group-info']
|
||||
for igroup_info in igroup_infos:
|
||||
if ('iscsi' != igroup_info['initiator-group-type'][0] or
|
||||
'linux' != igroup_info['initiator-group-os-type'][0]):
|
||||
continue
|
||||
igroup_name = igroup_info['initiator-group-name'][0]
|
||||
if not igroup_name.startswith('openstack-'):
|
||||
continue
|
||||
initiators = igroup_info['initiators'][0]['initiator-info']
|
||||
for initiator in initiators:
|
||||
if initiator_name == initiator['initiator-name'][0]:
|
||||
return igroup_name
|
||||
return None
|
||||
|
||||
def _create_igroup(self, host_id, initiator_name):
|
||||
"""
|
||||
Create a new igroup (initiator group) on the storage system to hold
|
||||
the given iSCSI initiator. The group will only have 1 member and will
|
||||
be named "openstack-${initiator_name}".
|
||||
"""
|
||||
igroup_name = 'openstack-' + initiator_name
|
||||
request = self.client.factory.create('Request')
|
||||
request.Name = 'igroup-create'
|
||||
igroup_create_xml = (
|
||||
'<initiator-group-name>%s</initiator-group-name>'
|
||||
'<initiator-group-type>iscsi</initiator-group-type>'
|
||||
'<os-type>linux</os-type><ostype>linux</ostype>')
|
||||
request.Args = text.Raw(igroup_create_xml % igroup_name)
|
||||
response = self.client.service.ApiProxy(Target=host_id,
|
||||
Request=request)
|
||||
self._check_fail(request, response)
|
||||
request = self.client.factory.create('Request')
|
||||
request.Name = 'igroup-add'
|
||||
igroup_add_xml = (
|
||||
'<initiator-group-name>%s</initiator-group-name>'
|
||||
'<initiator>%s</initiator>')
|
||||
request.Args = text.Raw(igroup_add_xml % (igroup_name, initiator_name))
|
||||
response = self.client.service.ApiProxy(Target=host_id,
|
||||
Request=request)
|
||||
self._check_fail(request, response)
|
||||
return igroup_name
|
||||
|
||||
def _get_lun_mappping(self, host_id, lunpath, igroup_name):
|
||||
"""
|
||||
Check if a given LUN is already mapped to the given igroup (initiator
|
||||
group). If the LUN is mapped, also return the LUN number for the
|
||||
mapping.
|
||||
"""
|
||||
request = self.client.factory.create('Request')
|
||||
request.Name = 'lun-map-list-info'
|
||||
request.Args = text.Raw('<path>%s</path>' % (lunpath))
|
||||
response = self.client.service.ApiProxy(Target=host_id,
|
||||
Request=request)
|
||||
self._check_fail(request, response)
|
||||
igroups = response.Results['initiator-groups']
|
||||
if self._api_elem_is_empty(igroups):
|
||||
return {'mapped': False}
|
||||
igroup_infos = igroups[0]['initiator-group-info']
|
||||
for igroup_info in igroup_infos:
|
||||
if igroup_name == igroup_info['initiator-group-name'][0]:
|
||||
return {'mapped': True, 'lun_num': igroup_info['lun-id'][0]}
|
||||
return {'mapped': False}
|
||||
|
||||
def _map_initiator(self, host_id, lunpath, igroup_name):
|
||||
"""
|
||||
Map the given LUN to the given igroup (initiator group). Return the LUN
|
||||
number that the LUN was mapped to (the filer will choose the lowest
|
||||
available number).
|
||||
"""
|
||||
request = self.client.factory.create('Request')
|
||||
request.Name = 'lun-map'
|
||||
lun_map_xml = ('<initiator-group>%s</initiator-group>'
|
||||
'<path>%s</path>')
|
||||
request.Args = text.Raw(lun_map_xml % (igroup_name, lunpath))
|
||||
response = self.client.service.ApiProxy(Target=host_id,
|
||||
Request=request)
|
||||
self._check_fail(request, response)
|
||||
return response.Results['lun-id-assigned'][0]
|
||||
|
||||
def _unmap_initiator(self, host_id, lunpath, igroup_name):
|
||||
"""Unmap the given LUN from the given igroup (initiator group)."""
|
||||
request = self.client.factory.create('Request')
|
||||
request.Name = 'lun-unmap'
|
||||
lun_unmap_xml = ('<initiator-group>%s</initiator-group>'
|
||||
'<path>%s</path>')
|
||||
request.Args = text.Raw(lun_unmap_xml % (igroup_name, lunpath))
|
||||
response = self.client.service.ApiProxy(Target=host_id,
|
||||
Request=request)
|
||||
self._check_fail(request, response)
|
||||
|
||||
def _ensure_initiator_mapped(self, host_id, lunpath, initiator_name):
|
||||
"""
|
||||
Check if a LUN is mapped to a given initiator already and create
|
||||
the mapping if it is not. A new igroup will be created if needed.
|
||||
Returns the LUN number for the mapping between the LUN and initiator
|
||||
in both cases.
|
||||
"""
|
||||
lunpath = '/vol/' + lunpath
|
||||
igroup_name = self._find_igroup_for_initiator(host_id, initiator_name)
|
||||
if not igroup_name:
|
||||
igroup_name = self._create_igroup(host_id, initiator_name)
|
||||
|
||||
mapping = self._get_lun_mappping(host_id, lunpath, igroup_name)
|
||||
if mapping['mapped']:
|
||||
return mapping['lun_num']
|
||||
return self._map_initiator(host_id, lunpath, igroup_name)
|
||||
|
||||
def _ensure_initiator_unmapped(self, host_id, lunpath, initiator_name):
|
||||
"""
|
||||
Check if a LUN is mapped to a given initiator and remove the
|
||||
mapping if it is. This does not destroy the igroup.
|
||||
"""
|
||||
lunpath = '/vol/' + lunpath
|
||||
igroup_name = self._find_igroup_for_initiator(host_id, initiator_name)
|
||||
if not igroup_name:
|
||||
return
|
||||
|
||||
mapping = self._get_lun_mappping(host_id, lunpath, igroup_name)
|
||||
if mapping['mapped']:
|
||||
self._unmap_initiator(host_id, lunpath, igroup_name)
|
||||
|
||||
def initialize_connection(self, volume, connector):
|
||||
"""
|
||||
Do the LUN masking on the storage system so the initiator can access
|
||||
the LUN on the target. Also return the iSCSI properties so the
|
||||
initiator can find the LUN. This implementation does not call
|
||||
_get_iscsi_properties() to get the properties because cannot store the
|
||||
LUN number in the database. We only find out what the LUN number will
|
||||
be during this method call so we construct the properties dictionary
|
||||
ourselves.
|
||||
"""
|
||||
initiator_name = connector['initiator']
|
||||
lun_id = volume['provider_location']
|
||||
if not lun_id:
|
||||
msg = _("No LUN ID for volume %s")
|
||||
raise exception.Error(msg % volume['name'])
|
||||
lun = self._get_lun_details(lun_id)
|
||||
if not lun:
|
||||
msg = _('Failed to get LUN details for LUN ID %s')
|
||||
raise exception.Error(msg % lun_id)
|
||||
lun_num = self._ensure_initiator_mapped(lun.HostId, lun.LunPath,
|
||||
initiator_name)
|
||||
|
||||
host = self._get_host_details(lun.HostId)
|
||||
if not host:
|
||||
msg = _('Failed to get host details for host ID %s')
|
||||
raise exception.Error(msg % lun.HostId)
|
||||
|
||||
portal = self._get_target_portal_for_host(host.HostId,
|
||||
host.HostAddress)
|
||||
if not portal:
|
||||
msg = _('Failed to get target portal for filer: %s')
|
||||
raise exception.Error(msg % host.HostName)
|
||||
|
||||
iqn = self._get_iqn_for_host(host.HostId)
|
||||
if not iqn:
|
||||
msg = _('Failed to get target IQN for filer: %s')
|
||||
raise exception.Error(msg % host.HostName)
|
||||
|
||||
properties = {}
|
||||
properties['target_discovered'] = False
|
||||
(address, port) = (portal['address'], portal['port'])
|
||||
properties['target_portal'] = '%s:%s' % (address, port)
|
||||
properties['target_iqn'] = iqn
|
||||
properties['target_lun'] = lun_num
|
||||
properties['volume_id'] = volume['id']
|
||||
|
||||
auth = volume['provider_auth']
|
||||
if auth:
|
||||
(auth_method, auth_username, auth_secret) = auth.split()
|
||||
|
||||
properties['auth_method'] = auth_method
|
||||
properties['auth_username'] = auth_username
|
||||
properties['auth_password'] = auth_secret
|
||||
|
||||
return {
|
||||
'driver_volume_type': 'iscsi',
|
||||
'data': properties,
|
||||
}
|
||||
|
||||
def terminate_connection(self, volume, connector):
|
||||
"""
|
||||
Unmask the LUN on the storage system so the given intiator can no
|
||||
longer access it.
|
||||
"""
|
||||
initiator_name = connector['initiator']
|
||||
lun_id = volume['provider_location']
|
||||
if not lun_id:
|
||||
msg = _('No LUN ID for volume %s')
|
||||
raise exception.Error(msg % (volume['name']))
|
||||
lun = self._get_lun_details(lun_id)
|
||||
if not lun:
|
||||
msg = _('Failed to get LUN details for LUN ID %s')
|
||||
raise exception.Error(msg % (lun_id))
|
||||
self._ensure_initiator_unmapped(lun.HostId, lun.LunPath,
|
||||
initiator_name)
|
||||
|
||||
def create_volume_from_snapshot(self, volume, snapshot):
|
||||
raise NotImplementedError()
|
||||
|
||||
def create_snapshot(self, snapshot):
|
||||
raise NotImplementedError()
|
||||
|
||||
def delete_snapshot(self, snapshot):
|
||||
raise NotImplementedError()
|
||||
|
||||
def check_for_export(self, context, volume_id):
|
||||
raise NotImplementedError()
|
Loading…
Reference in New Issue
Block a user