summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorNicholas Bellinger <nab@risingtidesystems.com>2011-05-04 21:00:00 +0000
committerNicholas Bellinger <nab@risingtidesystems.com>2011-05-04 21:00:00 +0000
commita0a62b501e314a607afb331c65f40772b5c4e464 (patch)
tree1bcafd25d9bd2d8be45e0cb621da57954782148f
downloadrtslib-fb-1.90.tar.gz
Initial rtslib commit1.90
Signed-off-by: Nicholas A. Bellinger <nab@risingtidesystems.com>
-rw-r--r--COPYING661
-rw-r--r--Makefile85
-rw-r--r--README24
-rwxr-xr-xbin/clean30
-rwxr-xr-xbin/gen_changelog45
-rwxr-xr-xbin/gen_changelog_cleanup20
-rwxr-xr-xbin/gendoc31
-rw-r--r--debian/README.Debian14
-rw-r--r--debian/compat1
-rw-r--r--debian/control19
-rw-r--r--debian/copyright13
-rw-r--r--debian/python-rtslib.dirs2
-rw-r--r--debian/python-rtslib.docs2
-rw-r--r--debian/python-rtslib.install2
-rwxr-xr-xdebian/python-rtslib.postinst17
-rwxr-xr-xdebian/python-rtslib.preinst3
-rwxr-xr-xdebian/python-rtslib.prerm8
-rw-r--r--debian/pyversions1
-rw-r--r--debian/rtslib-doc.docs4
-rwxr-xr-xdebian/rules49
-rw-r--r--rtslib/__init__.py35
-rw-r--r--rtslib/loop.py465
-rw-r--r--rtslib/node.py236
-rw-r--r--rtslib/root.py164
-rw-r--r--rtslib/target.py1157
-rw-r--r--rtslib/tcm.py1130
-rw-r--r--rtslib/utils.py630
-rwxr-xr-xsetup.py41
-rw-r--r--specs/README137
-rw-r--r--specs/example.spec.txt29
-rw-r--r--specs/ib_srpt.spec32
-rw-r--r--specs/iscsi.spec29
-rw-r--r--specs/loopback.spec28
-rw-r--r--specs/qla2xxx.spec28
-rw-r--r--specs/tcm_fc.spec30
35 files changed, 5202 insertions, 0 deletions
diff --git a/COPYING b/COPYING
new file mode 100644
index 0000000..dba13ed
--- /dev/null
+++ b/COPYING
@@ -0,0 +1,661 @@
+ GNU AFFERO GENERAL PUBLIC LICENSE
+ Version 3, 19 November 2007
+
+ Copyright (C) 2007 Free Software Foundation, Inc. <http://fsf.org/>
+ Everyone is permitted to copy and distribute verbatim copies
+ of this license document, but changing it is not allowed.
+
+ Preamble
+
+ The GNU Affero General Public License is a free, copyleft license for
+software and other kinds of works, specifically designed to ensure
+cooperation with the community in the case of network server software.
+
+ The licenses for most software and other practical works are designed
+to take away your freedom to share and change the works. By contrast,
+our General Public Licenses are intended to guarantee your freedom to
+share and change all versions of a program--to make sure it remains free
+software for all its users.
+
+ When we speak of free software, we are referring to freedom, not
+price. Our General Public Licenses are designed to make sure that you
+have the freedom to distribute copies of free software (and charge for
+them if you wish), that you receive source code or can get it if you
+want it, that you can change the software or use pieces of it in new
+free programs, and that you know you can do these things.
+
+ Developers that use our General Public Licenses protect your rights
+with two steps: (1) assert copyright on the software, and (2) offer
+you this License which gives you legal permission to copy, distribute
+and/or modify the software.
+
+ A secondary benefit of defending all users' freedom is that
+improvements made in alternate versions of the program, if they
+receive widespread use, become available for other developers to
+incorporate. Many developers of free software are heartened and
+encouraged by the resulting cooperation. However, in the case of
+software used on network servers, this result may fail to come about.
+The GNU General Public License permits making a modified version and
+letting the public access it on a server without ever releasing its
+source code to the public.
+
+ The GNU Affero General Public License is designed specifically to
+ensure that, in such cases, the modified source code becomes available
+to the community. It requires the operator of a network server to
+provide the source code of the modified version running there to the
+users of that server. Therefore, public use of a modified version, on
+a publicly accessible server, gives the public access to the source
+code of the modified version.
+
+ An older license, called the Affero General Public License and
+published by Affero, was designed to accomplish similar goals. This is
+a different license, not a version of the Affero GPL, but Affero has
+released a new version of the Affero GPL which permits relicensing under
+this license.
+
+ The precise terms and conditions for copying, distribution and
+modification follow.
+
+ TERMS AND CONDITIONS
+
+ 0. Definitions.
+
+ "This License" refers to version 3 of the GNU Affero General Public License.
+
+ "Copyright" also means copyright-like laws that apply to other kinds of
+works, such as semiconductor masks.
+
+ "The Program" refers to any copyrightable work licensed under this
+License. Each licensee is addressed as "you". "Licensees" and
+"recipients" may be individuals or organizations.
+
+ To "modify" a work means to copy from or adapt all or part of the work
+in a fashion requiring copyright permission, other than the making of an
+exact copy. The resulting work is called a "modified version" of the
+earlier work or a work "based on" the earlier work.
+
+ A "covered work" means either the unmodified Program or a work based
+on the Program.
+
+ To "propagate" a work means to do anything with it that, without
+permission, would make you directly or secondarily liable for
+infringement under applicable copyright law, except executing it on a
+computer or modifying a private copy. Propagation includes copying,
+distribution (with or without modification), making available to the
+public, and in some countries other activities as well.
+
+ To "convey" a work means any kind of propagation that enables other
+parties to make or receive copies. Mere interaction with a user through
+a computer network, with no transfer of a copy, is not conveying.
+
+ An interactive user interface displays "Appropriate Legal Notices"
+to the extent that it includes a convenient and prominently visible
+feature that (1) displays an appropriate copyright notice, and (2)
+tells the user that there is no warranty for the work (except to the
+extent that warranties are provided), that licensees may convey the
+work under this License, and how to view a copy of this License. If
+the interface presents a list of user commands or options, such as a
+menu, a prominent item in the list meets this criterion.
+
+ 1. Source Code.
+
+ The "source code" for a work means the preferred form of the work
+for making modifications to it. "Object code" means any non-source
+form of a work.
+
+ A "Standard Interface" means an interface that either is an official
+standard defined by a recognized standards body, or, in the case of
+interfaces specified for a particular programming language, one that
+is widely used among developers working in that language.
+
+ The "System Libraries" of an executable work include anything, other
+than the work as a whole, that (a) is included in the normal form of
+packaging a Major Component, but which is not part of that Major
+Component, and (b) serves only to enable use of the work with that
+Major Component, or to implement a Standard Interface for which an
+implementation is available to the public in source code form. A
+"Major Component", in this context, means a major essential component
+(kernel, window system, and so on) of the specific operating system
+(if any) on which the executable work runs, or a compiler used to
+produce the work, or an object code interpreter used to run it.
+
+ The "Corresponding Source" for a work in object code form means all
+the source code needed to generate, install, and (for an executable
+work) run the object code and to modify the work, including scripts to
+control those activities. However, it does not include the work's
+System Libraries, or general-purpose tools or generally available free
+programs which are used unmodified in performing those activities but
+which are not part of the work. For example, Corresponding Source
+includes interface definition files associated with source files for
+the work, and the source code for shared libraries and dynamically
+linked subprograms that the work is specifically designed to require,
+such as by intimate data communication or control flow between those
+subprograms and other parts of the work.
+
+ The Corresponding Source need not include anything that users
+can regenerate automatically from other parts of the Corresponding
+Source.
+
+ The Corresponding Source for a work in source code form is that
+same work.
+
+ 2. Basic Permissions.
+
+ All rights granted under this License are granted for the term of
+copyright on the Program, and are irrevocable provided the stated
+conditions are met. This License explicitly affirms your unlimited
+permission to run the unmodified Program. The output from running a
+covered work is covered by this License only if the output, given its
+content, constitutes a covered work. This License acknowledges your
+rights of fair use or other equivalent, as provided by copyright law.
+
+ You may make, run and propagate covered works that you do not
+convey, without conditions so long as your license otherwise remains
+in force. You may convey covered works to others for the sole purpose
+of having them make modifications exclusively for you, or provide you
+with facilities for running those works, provided that you comply with
+the terms of this License in conveying all material for which you do
+not control copyright. Those thus making or running the covered works
+for you must do so exclusively on your behalf, under your direction
+and control, on terms that prohibit them from making any copies of
+your copyrighted material outside their relationship with you.
+
+ Conveying under any other circumstances is permitted solely under
+the conditions stated below. Sublicensing is not allowed; section 10
+makes it unnecessary.
+
+ 3. Protecting Users' Legal Rights From Anti-Circumvention Law.
+
+ No covered work shall be deemed part of an effective technological
+measure under any applicable law fulfilling obligations under article
+11 of the WIPO copyright treaty adopted on 20 December 1996, or
+similar laws prohibiting or restricting circumvention of such
+measures.
+
+ When you convey a covered work, you waive any legal power to forbid
+circumvention of technological measures to the extent such circumvention
+is effected by exercising rights under this License with respect to
+the covered work, and you disclaim any intention to limit operation or
+modification of the work as a means of enforcing, against the work's
+users, your or third parties' legal rights to forbid circumvention of
+technological measures.
+
+ 4. Conveying Verbatim Copies.
+
+ You may convey verbatim copies of the Program's source code as you
+receive it, in any medium, provided that you conspicuously and
+appropriately publish on each copy an appropriate copyright notice;
+keep intact all notices stating that this License and any
+non-permissive terms added in accord with section 7 apply to the code;
+keep intact all notices of the absence of any warranty; and give all
+recipients a copy of this License along with the Program.
+
+ You may charge any price or no price for each copy that you convey,
+and you may offer support or warranty protection for a fee.
+
+ 5. Conveying Modified Source Versions.
+
+ You may convey a work based on the Program, or the modifications to
+produce it from the Program, in the form of source code under the
+terms of section 4, provided that you also meet all of these conditions:
+
+ a) The work must carry prominent notices stating that you modified
+ it, and giving a relevant date.
+
+ b) The work must carry prominent notices stating that it is
+ released under this License and any conditions added under section
+ 7. This requirement modifies the requirement in section 4 to
+ "keep intact all notices".
+
+ c) You must license the entire work, as a whole, under this
+ License to anyone who comes into possession of a copy. This
+ License will therefore apply, along with any applicable section 7
+ additional terms, to the whole of the work, and all its parts,
+ regardless of how they are packaged. This License gives no
+ permission to license the work in any other way, but it does not
+ invalidate such permission if you have separately received it.
+
+ d) If the work has interactive user interfaces, each must display
+ Appropriate Legal Notices; however, if the Program has interactive
+ interfaces that do not display Appropriate Legal Notices, your
+ work need not make them do so.
+
+ A compilation of a covered work with other separate and independent
+works, which are not by their nature extensions of the covered work,
+and which are not combined with it such as to form a larger program,
+in or on a volume of a storage or distribution medium, is called an
+"aggregate" if the compilation and its resulting copyright are not
+used to limit the access or legal rights of the compilation's users
+beyond what the individual works permit. Inclusion of a covered work
+in an aggregate does not cause this License to apply to the other
+parts of the aggregate.
+
+ 6. Conveying Non-Source Forms.
+
+ You may convey a covered work in object code form under the terms
+of sections 4 and 5, provided that you also convey the
+machine-readable Corresponding Source under the terms of this License,
+in one of these ways:
+
+ a) Convey the object code in, or embodied in, a physical product
+ (including a physical distribution medium), accompanied by the
+ Corresponding Source fixed on a durable physical medium
+ customarily used for software interchange.
+
+ b) Convey the object code in, or embodied in, a physical product
+ (including a physical distribution medium), accompanied by a
+ written offer, valid for at least three years and valid for as
+ long as you offer spare parts or customer support for that product
+ model, to give anyone who possesses the object code either (1) a
+ copy of the Corresponding Source for all the software in the
+ product that is covered by this License, on a durable physical
+ medium customarily used for software interchange, for a price no
+ more than your reasonable cost of physically performing this
+ conveying of source, or (2) access to copy the
+ Corresponding Source from a network server at no charge.
+
+ c) Convey individual copies of the object code with a copy of the
+ written offer to provide the Corresponding Source. This
+ alternative is allowed only occasionally and noncommercially, and
+ only if you received the object code with such an offer, in accord
+ with subsection 6b.
+
+ d) Convey the object code by offering access from a designated
+ place (gratis or for a charge), and offer equivalent access to the
+ Corresponding Source in the same way through the same place at no
+ further charge. You need not require recipients to copy the
+ Corresponding Source along with the object code. If the place to
+ copy the object code is a network server, the Corresponding Source
+ may be on a different server (operated by you or a third party)
+ that supports equivalent copying facilities, provided you maintain
+ clear directions next to the object code saying where to find the
+ Corresponding Source. Regardless of what server hosts the
+ Corresponding Source, you remain obligated to ensure that it is
+ available for as long as needed to satisfy these requirements.
+
+ e) Convey the object code using peer-to-peer transmission, provided
+ you inform other peers where the object code and Corresponding
+ Source of the work are being offered to the general public at no
+ charge under subsection 6d.
+
+ A separable portion of the object code, whose source code is excluded
+from the Corresponding Source as a System Library, need not be
+included in conveying the object code work.
+
+ A "User Product" is either (1) a "consumer product", which means any
+tangible personal property which is normally used for personal, family,
+or household purposes, or (2) anything designed or sold for incorporation
+into a dwelling. In determining whether a product is a consumer product,
+doubtful cases shall be resolved in favor of coverage. For a particular
+product received by a particular user, "normally used" refers to a
+typical or common use of that class of product, regardless of the status
+of the particular user or of the way in which the particular user
+actually uses, or expects or is expected to use, the product. A product
+is a consumer product regardless of whether the product has substantial
+commercial, industrial or non-consumer uses, unless such uses represent
+the only significant mode of use of the product.
+
+ "Installation Information" for a User Product means any methods,
+procedures, authorization keys, or other information required to install
+and execute modified versions of a covered work in that User Product from
+a modified version of its Corresponding Source. The information must
+suffice to ensure that the continued functioning of the modified object
+code is in no case prevented or interfered with solely because
+modification has been made.
+
+ If you convey an object code work under this section in, or with, or
+specifically for use in, a User Product, and the conveying occurs as
+part of a transaction in which the right of possession and use of the
+User Product is transferred to the recipient in perpetuity or for a
+fixed term (regardless of how the transaction is characterized), the
+Corresponding Source conveyed under this section must be accompanied
+by the Installation Information. But this requirement does not apply
+if neither you nor any third party retains the ability to install
+modified object code on the User Product (for example, the work has
+been installed in ROM).
+
+ The requirement to provide Installation Information does not include a
+requirement to continue to provide support service, warranty, or updates
+for a work that has been modified or installed by the recipient, or for
+the User Product in which it has been modified or installed. Access to a
+network may be denied when the modification itself materially and
+adversely affects the operation of the network or violates the rules and
+protocols for communication across the network.
+
+ Corresponding Source conveyed, and Installation Information provided,
+in accord with this section must be in a format that is publicly
+documented (and with an implementation available to the public in
+source code form), and must require no special password or key for
+unpacking, reading or copying.
+
+ 7. Additional Terms.
+
+ "Additional permissions" are terms that supplement the terms of this
+License by making exceptions from one or more of its conditions.
+Additional permissions that are applicable to the entire Program shall
+be treated as though they were included in this License, to the extent
+that they are valid under applicable law. If additional permissions
+apply only to part of the Program, that part may be used separately
+under those permissions, but the entire Program remains governed by
+this License without regard to the additional permissions.
+
+ When you convey a copy of a covered work, you may at your option
+remove any additional permissions from that copy, or from any part of
+it. (Additional permissions may be written to require their own
+removal in certain cases when you modify the work.) You may place
+additional permissions on material, added by you to a covered work,
+for which you have or can give appropriate copyright permission.
+
+ Notwithstanding any other provision of this License, for material you
+add to a covered work, you may (if authorized by the copyright holders of
+that material) supplement the terms of this License with terms:
+
+ a) Disclaiming warranty or limiting liability differently from the
+ terms of sections 15 and 16 of this License; or
+
+ b) Requiring preservation of specified reasonable legal notices or
+ author attributions in that material or in the Appropriate Legal
+ Notices displayed by works containing it; or
+
+ c) Prohibiting misrepresentation of the origin of that material, or
+ requiring that modified versions of such material be marked in
+ reasonable ways as different from the original version; or
+
+ d) Limiting the use for publicity purposes of names of licensors or
+ authors of the material; or
+
+ e) Declining to grant rights under trademark law for use of some
+ trade names, trademarks, or service marks; or
+
+ f) Requiring indemnification of licensors and authors of that
+ material by anyone who conveys the material (or modified versions of
+ it) with contractual assumptions of liability to the recipient, for
+ any liability that these contractual assumptions directly impose on
+ those licensors and authors.
+
+ All other non-permissive additional terms are considered "further
+restrictions" within the meaning of section 10. If the Program as you
+received it, or any part of it, contains a notice stating that it is
+governed by this License along with a term that is a further
+restriction, you may remove that term. If a license document contains
+a further restriction but permits relicensing or conveying under this
+License, you may add to a covered work material governed by the terms
+of that license document, provided that the further restriction does
+not survive such relicensing or conveying.
+
+ If you add terms to a covered work in accord with this section, you
+must place, in the relevant source files, a statement of the
+additional terms that apply to those files, or a notice indicating
+where to find the applicable terms.
+
+ Additional terms, permissive or non-permissive, may be stated in the
+form of a separately written license, or stated as exceptions;
+the above requirements apply either way.
+
+ 8. Termination.
+
+ You may not propagate or modify a covered work except as expressly
+provided under this License. Any attempt otherwise to propagate or
+modify it is void, and will automatically terminate your rights under
+this License (including any patent licenses granted under the third
+paragraph of section 11).
+
+ However, if you cease all violation of this License, then your
+license from a particular copyright holder is reinstated (a)
+provisionally, unless and until the copyright holder explicitly and
+finally terminates your license, and (b) permanently, if the copyright
+holder fails to notify you of the violation by some reasonable means
+prior to 60 days after the cessation.
+
+ Moreover, your license from a particular copyright holder is
+reinstated permanently if the copyright holder notifies you of the
+violation by some reasonable means, this is the first time you have
+received notice of violation of this License (for any work) from that
+copyright holder, and you cure the violation prior to 30 days after
+your receipt of the notice.
+
+ Termination of your rights under this section does not terminate the
+licenses of parties who have received copies or rights from you under
+this License. If your rights have been terminated and not permanently
+reinstated, you do not qualify to receive new licenses for the same
+material under section 10.
+
+ 9. Acceptance Not Required for Having Copies.
+
+ You are not required to accept this License in order to receive or
+run a copy of the Program. Ancillary propagation of a covered work
+occurring solely as a consequence of using peer-to-peer transmission
+to receive a copy likewise does not require acceptance. However,
+nothing other than this License grants you permission to propagate or
+modify any covered work. These actions infringe copyright if you do
+not accept this License. Therefore, by modifying or propagating a
+covered work, you indicate your acceptance of this License to do so.
+
+ 10. Automatic Licensing of Downstream Recipients.
+
+ Each time you convey a covered work, the recipient automatically
+receives a license from the original licensors, to run, modify and
+propagate that work, subject to this License. You are not responsible
+for enforcing compliance by third parties with this License.
+
+ An "entity transaction" is a transaction transferring control of an
+organization, or substantially all assets of one, or subdividing an
+organization, or merging organizations. If propagation of a covered
+work results from an entity transaction, each party to that
+transaction who receives a copy of the work also receives whatever
+licenses to the work the party's predecessor in interest had or could
+give under the previous paragraph, plus a right to possession of the
+Corresponding Source of the work from the predecessor in interest, if
+the predecessor has it or can get it with reasonable efforts.
+
+ You may not impose any further restrictions on the exercise of the
+rights granted or affirmed under this License. For example, you may
+not impose a license fee, royalty, or other charge for exercise of
+rights granted under this License, and you may not initiate litigation
+(including a cross-claim or counterclaim in a lawsuit) alleging that
+any patent claim is infringed by making, using, selling, offering for
+sale, or importing the Program or any portion of it.
+
+ 11. Patents.
+
+ A "contributor" is a copyright holder who authorizes use under this
+License of the Program or a work on which the Program is based. The
+work thus licensed is called the contributor's "contributor version".
+
+ A contributor's "essential patent claims" are all patent claims
+owned or controlled by the contributor, whether already acquired or
+hereafter acquired, that would be infringed by some manner, permitted
+by this License, of making, using, or selling its contributor version,
+but do not include claims that would be infringed only as a
+consequence of further modification of the contributor version. For
+purposes of this definition, "control" includes the right to grant
+patent sublicenses in a manner consistent with the requirements of
+this License.
+
+ Each contributor grants you a non-exclusive, worldwide, royalty-free
+patent license under the contributor's essential patent claims, to
+make, use, sell, offer for sale, import and otherwise run, modify and
+propagate the contents of its contributor version.
+
+ In the following three paragraphs, a "patent license" is any express
+agreement or commitment, however denominated, not to enforce a patent
+(such as an express permission to practice a patent or covenant not to
+sue for patent infringement). To "grant" such a patent license to a
+party means to make such an agreement or commitment not to enforce a
+patent against the party.
+
+ If you convey a covered work, knowingly relying on a patent license,
+and the Corresponding Source of the work is not available for anyone
+to copy, free of charge and under the terms of this License, through a
+publicly available network server or other readily accessible means,
+then you must either (1) cause the Corresponding Source to be so
+available, or (2) arrange to deprive yourself of the benefit of the
+patent license for this particular work, or (3) arrange, in a manner
+consistent with the requirements of this License, to extend the patent
+license to downstream recipients. "Knowingly relying" means you have
+actual knowledge that, but for the patent license, your conveying the
+covered work in a country, or your recipient's use of the covered work
+in a country, would infringe one or more identifiable patents in that
+country that you have reason to believe are valid.
+
+ If, pursuant to or in connection with a single transaction or
+arrangement, you convey, or propagate by procuring conveyance of, a
+covered work, and grant a patent license to some of the parties
+receiving the covered work authorizing them to use, propagate, modify
+or convey a specific copy of the covered work, then the patent license
+you grant is automatically extended to all recipients of the covered
+work and works based on it.
+
+ A patent license is "discriminatory" if it does not include within
+the scope of its coverage, prohibits the exercise of, or is
+conditioned on the non-exercise of one or more of the rights that are
+specifically granted under this License. You may not convey a covered
+work if you are a party to an arrangement with a third party that is
+in the business of distributing software, under which you make payment
+to the third party based on the extent of your activity of conveying
+the work, and under which the third party grants, to any of the
+parties who would receive the covered work from you, a discriminatory
+patent license (a) in connection with copies of the covered work
+conveyed by you (or copies made from those copies), or (b) primarily
+for and in connection with specific products or compilations that
+contain the covered work, unless you entered into that arrangement,
+or that patent license was granted, prior to 28 March 2007.
+
+ Nothing in this License shall be construed as excluding or limiting
+any implied license or other defenses to infringement that may
+otherwise be available to you under applicable patent law.
+
+ 12. No Surrender of Others' Freedom.
+
+ If conditions are imposed on you (whether by court order, agreement or
+otherwise) that contradict the conditions of this License, they do not
+excuse you from the conditions of this License. If you cannot convey a
+covered work so as to satisfy simultaneously your obligations under this
+License and any other pertinent obligations, then as a consequence you may
+not convey it at all. For example, if you agree to terms that obligate you
+to collect a royalty for further conveying from those to whom you convey
+the Program, the only way you could satisfy both those terms and this
+License would be to refrain entirely from conveying the Program.
+
+ 13. Remote Network Interaction; Use with the GNU General Public License.
+
+ Notwithstanding any other provision of this License, if you modify the
+Program, your modified version must prominently offer all users
+interacting with it remotely through a computer network (if your version
+supports such interaction) an opportunity to receive the Corresponding
+Source of your version by providing access to the Corresponding Source
+from a network server at no charge, through some standard or customary
+means of facilitating copying of software. This Corresponding Source
+shall include the Corresponding Source for any work covered by version 3
+of the GNU General Public License that is incorporated pursuant to the
+following paragraph.
+
+ Notwithstanding any other provision of this License, you have
+permission to link or combine any covered work with a work licensed
+under version 3 of the GNU General Public License into a single
+combined work, and to convey the resulting work. The terms of this
+License will continue to apply to the part which is the covered work,
+but the work with which it is combined will remain governed by version
+3 of the GNU General Public License.
+
+ 14. Revised Versions of this License.
+
+ The Free Software Foundation may publish revised and/or new versions of
+the GNU Affero General Public License from time to time. Such new versions
+will be similar in spirit to the present version, but may differ in detail to
+address new problems or concerns.
+
+ Each version is given a distinguishing version number. If the
+Program specifies that a certain numbered version of the GNU Affero General
+Public License "or any later version" applies to it, you have the
+option of following the terms and conditions either of that numbered
+version or of any later version published by the Free Software
+Foundation. If the Program does not specify a version number of the
+GNU Affero General Public License, you may choose any version ever published
+by the Free Software Foundation.
+
+ If the Program specifies that a proxy can decide which future
+versions of the GNU Affero General Public License can be used, that proxy's
+public statement of acceptance of a version permanently authorizes you
+to choose that version for the Program.
+
+ Later license versions may give you additional or different
+permissions. However, no additional obligations are imposed on any
+author or copyright holder as a result of your choosing to follow a
+later version.
+
+ 15. Disclaimer of Warranty.
+
+ THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY
+APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT
+HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY
+OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO,
+THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
+PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM
+IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF
+ALL NECESSARY SERVICING, REPAIR OR CORRECTION.
+
+ 16. Limitation of Liability.
+
+ IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
+WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS
+THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY
+GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE
+USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF
+DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD
+PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS),
+EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF
+SUCH DAMAGES.
+
+ 17. Interpretation of Sections 15 and 16.
+
+ If the disclaimer of warranty and limitation of liability provided
+above cannot be given local legal effect according to their terms,
+reviewing courts shall apply local law that most closely approximates
+an absolute waiver of all civil liability in connection with the
+Program, unless a warranty or assumption of liability accompanies a
+copy of the Program in return for a fee.
+
+ END OF TERMS AND CONDITIONS
+
+ How to Apply These Terms to Your New Programs
+
+ If you develop a new program, and you want it to be of the greatest
+possible use to the public, the best way to achieve this is to make it
+free software which everyone can redistribute and change under these terms.
+
+ To do so, attach the following notices to the program. It is safest
+to attach them to the start of each source file to most effectively
+state the exclusion of warranty; and each file should have at least
+the "copyright" line and a pointer to where the full notice is found.
+
+ <one line to give the program's name and a brief idea of what it does.>
+ Copyright (C) <year> <name of author>
+
+ This program is free software: you can redistribute it and/or modify
+ it under the terms of the GNU Affero General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ This program is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU Affero General Public License for more details.
+
+ You should have received a copy of the GNU Affero General Public License
+ along with this program. If not, see <http://www.gnu.org/licenses/>.
+
+Also add information on how to contact you by electronic and paper mail.
+
+ If your software can interact with users remotely through a computer
+network, you should also make sure that it provides a way for users to
+get its source. For example, if your program is a web application, its
+interface could display a "Source" link that leads users to an archive
+of the code. There are many ways you could offer source, and different
+solutions will be better for different programs; see section 13 for the
+specific requirements.
+
+ You should also get your employer (if you work as a programmer) or school,
+if any, to sign a "copyright disclaimer" for the program, if necessary.
+For more information on this, and how to apply and follow the GNU AGPL, see
+<http://www.gnu.org/licenses/>.
diff --git a/Makefile b/Makefile
new file mode 100644
index 0000000..389c2c2
--- /dev/null
+++ b/Makefile
@@ -0,0 +1,85 @@
+# This file is part of RTSLib Community Edition.
+# Copyright (c) 2011 by RisingTide Systems LLC
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU Affero General Public License as
+# published by the Free Software Foundation, version 3 (AGPLv3).
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU Affero General Public License for more details.
+#
+# You should have received a copy of the GNU Affero General Public License
+# along with this program. If not, see <http://www.gnu.org/licenses/>.
+
+NAME = rtslib
+LIB = /usr/share
+DOC = ${LIB}/doc/
+SETUP = ./setup.py
+CLEAN = ./bin/clean
+GENDOC = ./bin/gendoc
+
+all: usage
+usage:
+ @echo "Usage:"
+ @echo " make install - Install rtslib"
+ @echo " make installdocs - Install the documentation"
+ @echo "Developer targets:"
+ @echo " make packages - Generate the Debian and RPM packages"
+ @echo " make doc - Generate the documentation"
+ @echo " make clean - Cleanup the local repository"
+ @echo " make sdist - Build the source tarball"
+ @echo " make bdist - Build the installable tarball"
+
+install:
+ ${SETUP} install
+
+doc:
+ ./bin/gen_changelog
+ ${GENDOC}
+
+installdocs: doc
+ @test -e ${DOC} || \
+ echo "Could not find ${DOC}; check the makefile variables."
+ @test -e ${DOC}
+ cp -r doc/* ${DOC}/${NAME}/
+
+clean:
+ ${CLEAN}
+ ./bin/gen_changelog_cleanup
+
+packages: clean doc
+ dpkg-buildpackage -rfakeroot | tee dpkg-buildpackage.log
+ ./bin/gen_changelog_cleanup
+ grep "source version" dpkg-buildpackage.log | awk '{print $$4}' > dpkg-buildpackage.version
+ @test -e dist || mkdir dist
+ mv ../${NAME}_$$(cat dpkg-buildpackage.version).dsc dist
+ mv ../${NAME}_$$(cat dpkg-buildpackage.version)_*.changes dist
+ mv ../${NAME}_$$(cat dpkg-buildpackage.version).tar.gz dist
+ mv ../*${NAME}*$$(cat dpkg-buildpackage.version)*.deb dist
+ @test -e build || mkdir build
+ cd build; alien --scripts -k -g -r ../dist/rtslib-doc_$$(cat ../dpkg-buildpackage.version)_all.deb
+ cd build/rtslib-doc-*; mkdir usr/share/doc/packages
+ cd build/rtslib-doc-*; mv usr/share/doc/rtslib-doc usr/share/doc/packages/
+ cd build/rtslib-doc-*; perl -pi -e "s,/usr/share/doc/rtslib-doc,/usr/share/doc/packages/rtslib-doc,g" *.spec
+ cd build/rtslib-doc-*; perl -pi -e "s,%%{ARCH},noarch,g" *.spec
+ cd build/rtslib-doc-*; perl -pi -e "s,%post,%posttrans,g" *.spec
+ cd build/rtslib-doc-*; rpmbuild --buildroot $$PWD -bb *.spec
+ cd build; alien --scripts -k -g -r ../dist/python-rtslib_$$(cat ../dpkg-buildpackage.version)_all.deb; cd ..
+ cd build/python-rtslib-*; mkdir usr/share/doc/packages
+ cd build/python-rtslib-*; mv usr/share/doc/python-rtslib usr/share/doc/packages/
+ cd build/python-rtslib-*; perl -pi -e "s,/usr/share/doc/python-rtslib,/usr/share/doc/packages/python-rtslib,g" *.spec
+ cd build/python-rtslib-*; perl -pi -e 's/Group:/Requires: python >= 2.5\nGroup:/g' *.spec
+ cd build/python-rtslib-*; perl -pi -e "s,%%{ARCH},noarch,g" *.spec
+ cd build/python-rtslib-*; perl -pi -e "s,%post,%posttrans,g" *.spec
+ cd build/python-rtslib-*; rpmbuild --buildroot $$PWD -bb *.spec
+ mv build/*.rpm dist
+ rm dpkg-buildpackage.log dpkg-buildpackage.version
+
+sdist: clean doc
+ ${SETUP} sdist
+
+bdist: clean doc
+ ${SETUP} bdist
+
diff --git a/README b/README
new file mode 100644
index 0000000..410940f
--- /dev/null
+++ b/README
@@ -0,0 +1,24 @@
+# This file is part of RTSLib Community Edition.
+# Copyright (c) 2011 by RisingTide Systems LLC
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU Affero General Public License as
+# published by the Free Software Foundation, version 3 (AGPLv3).
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU Affero General Public License for more details.
+#
+# You should have received a copy of the GNU Affero General Public License
+# along with this program. If not, see <http://www.gnu.org/licenses/>.
+
+RTSLib Community Edition is a python library that provides an object API to
+RisingTide Systems generic SCSI Target as well as third-party target fabric
+modules written for it and backend storage objects.
+
+It is useful for developing 3rd-party applications, as well as serving as a
+foundation for RisingTide Systems userspace tools.
+
+For more information, see the rtslib API reference, available in both html
+and pdf formats as a separate package.
diff --git a/bin/clean b/bin/clean
new file mode 100755
index 0000000..40f4104
--- /dev/null
+++ b/bin/clean
@@ -0,0 +1,30 @@
+#!/bin/bash
+
+# This file is part of RTSLib Community Edition.
+# Copyright (c) 2011 by RisingTide Systems LLC
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU Affero General Public License as
+# published by the Free Software Foundation, version 3 (AGPLv3).
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU Affero General Public License for more details.
+#
+# You should have received a copy of the GNU Affero General Public License
+# along with this program. If not, see <http://www.gnu.org/licenses/>.
+
+rm -v rtslib/*.pyc rtslib/*.html 2>/dev/null
+rm -rvf doc doc/*.pdf doc/html doc/pdf doc/*.html 2>/dev/null
+rm -rfv rtslib.egg-info MANIFEST build dist 2>/dev/null
+rm -rvf pdf html 2>/dev/null
+rm -rvf debian/tmp 2>/dev/null
+rm -v build-stamp 2>/dev/null
+rm -v dpkg-buildpackage.log dpkg-buildpackage.version 2>/dev/null
+rm -rfv *.rpm 2>/dev/null
+rm -v debian/files debian/*.log debian/*.substvars 2>/dev/null
+rm -rv debian/rtslib-doc/ debian/python2.5-rtslib/ debian/python2.6-rtslib/ debian/python-rtslib/ 2>/dev/null
+rm -rv results 2>/dev/null
+echo "Finished cleanup."
+
diff --git a/bin/gen_changelog b/bin/gen_changelog
new file mode 100755
index 0000000..0aa9a4d
--- /dev/null
+++ b/bin/gen_changelog
@@ -0,0 +1,45 @@
+#!/bin/bash
+
+# This file is part of RTSLib Community Edition.
+# Copyright (c) 2011 by RisingTide Systems LLC
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU Affero General Public License as
+# published by the Free Software Foundation, version 3 (AGPLv3).
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU Affero General Public License for more details.
+#
+# You should have received a copy of the GNU Affero General Public License
+# along with this program. If not, see <http://www.gnu.org/licenses/>.
+
+NAME=$(git config --get user.name)
+EMAIL=$(git config --get user.email)
+DATE=$(date +'%a, %d %b %Y %H:%M:%S %z')
+
+SRCPKG=$(cat debian/control | grep ^Source | awk '{print $2}')
+
+LAST_TAG=$(git tag | tail -1)
+if [ -z $LAST_TAG ]; then
+ LAST_TAG='0.0'
+fi
+
+COMMIT=$(git log ${LAST_TAG}..HEAD | head -1 | colrm 1 7 | colrm 8)
+TS=$(date +%Y%m%d%H%M%S)
+
+if [ -z $COMMIT ]; then
+ VERSION="${LAST_TAG}"
+else
+ VERSION="${LAST_TAG}-${TS}.${COMMIT}"
+fi
+
+sed -i "s/__version__ = .*/__version__ = '${VERSION}'/g" ${SRCPKG}/__init__.py
+
+echo "${SRCPKG} (${VERSION}) unstable; urgency=low" > debian/changelog
+echo >> debian/changelog
+echo " * Generated package." >> debian/changelog
+echo >> debian/changelog
+echo " -- ${NAME} <${EMAIL}> ${DATE}" >> debian/changelog
+
diff --git a/bin/gen_changelog_cleanup b/bin/gen_changelog_cleanup
new file mode 100755
index 0000000..6d21485
--- /dev/null
+++ b/bin/gen_changelog_cleanup
@@ -0,0 +1,20 @@
+#!/bin/bash
+
+# This file is part of RTSLib Community Edition.
+# Copyright (c) 2011 by RisingTide Systems LLC
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU Affero General Public License as
+# published by the Free Software Foundation, version 3 (AGPLv3).
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU Affero General Public License for more details.
+#
+# You should have received a copy of the GNU Affero General Public License
+# along with this program. If not, see <http://www.gnu.org/licenses/>.
+
+SRCPKG=$(cat debian/control | grep ^Source | awk '{print $2}')
+rm -f debian/changelog
+sed -i "s/__version__ = .*/__version__ = 'GIT_VERSION'/g" ${SRCPKG}/__init__.py
diff --git a/bin/gendoc b/bin/gendoc
new file mode 100755
index 0000000..b4d8f46
--- /dev/null
+++ b/bin/gendoc
@@ -0,0 +1,31 @@
+#!/bin/bash
+
+# This file is part of RTSLib Community Edition.
+# Copyright (c) 2011 by RisingTide Systems LLC
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU Affero General Public License as
+# published by the Free Software Foundation, version 3 (AGPLv3).
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU Affero General Public License for more details.
+#
+# You should have received a copy of the GNU Affero General Public License
+# along with this program. If not, see <http://www.gnu.org/licenses/>.
+
+path=$PWD
+options='-n rtslib --exclude configobj rtslib/*.py'
+
+rm -rf doc &>/dev/null
+mkdir doc
+epydoc --no-sourcecode --pdf -v $options
+mkdir doc/pdf 2>/dev/null
+mv pdf/api.pdf doc/pdf/rtslib-API-reference.pdf
+rm pdf -rf
+epydoc --no-sourcecode --html $options
+mv html doc/
+perl -pi -e "s/<\?/<!/g" doc/html/*.html
+perl -pi -e "s/\?>/>/g" doc/html/*.html
+cp README COPYING doc/
diff --git a/debian/README.Debian b/debian/README.Debian
new file mode 100644
index 0000000..d28874c
--- /dev/null
+++ b/debian/README.Debian
@@ -0,0 +1,14 @@
+Copyright (c) 2011 by RisingTide Systems LLC
+
+This program is free software: you can redistribute it and/or modify
+it under the terms of the GNU Affero General Public License as
+published by the Free Software Foundation, version 3 (AGPLv3).
+
+This program is distributed in the hope that it will be useful,
+but WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+GNU Affero General Public License for more details.
+
+You should have received a copy of the GNU Affero General Public License
+along with this program. If not, see <http://www.gnu.org/licenses/>.
+
diff --git a/debian/compat b/debian/compat
new file mode 100644
index 0000000..7f8f011
--- /dev/null
+++ b/debian/compat
@@ -0,0 +1 @@
+7
diff --git a/debian/control b/debian/control
new file mode 100644
index 0000000..15bd90c
--- /dev/null
+++ b/debian/control
@@ -0,0 +1,19 @@
+Source: rtslib
+Section: python
+Priority: optional
+Maintainer: Jerome Martin <jxm@risingtidesystems.com>
+Build-Depends: debhelper(>= 7.0.1), python2.5, python2.6, python-epydoc
+Standards-Version: 3.8.1
+
+Package: python-rtslib
+Architecture: all
+Depends: python (>= 2.5)|python2.5|python2.6, python-configobj
+Suggests: rtslib-doc
+Conflicts: rtsadmin-frozen
+Description: RisingTide Systems generic SCSI target API in python.
+
+Package: rtslib-doc
+Section: doc
+Architecture: all
+Recommends: iceweasel | www-browser
+Description: RisingTide Systems generic SCSI target API documentation.
diff --git a/debian/copyright b/debian/copyright
new file mode 100644
index 0000000..b8b83d1
--- /dev/null
+++ b/debian/copyright
@@ -0,0 +1,13 @@
+This package was originally debianized by Jerome Martin <jxm@risingtidesystems.com>
+on Fri Nov 18 12:00:01 UTC 2009. It is currently maintained by Jerome Martin
+<jxm@risingtidesystems.com>.
+
+Upstream Author: Jerome Martin <jxm@risingtidesystems.com>
+
+Copyright:
+
+ Copyright (c) 2009 by RisingTide Systems LLC.
+ All rights reserved.
+ For licensing information, please contact us.
+ Not for redistribution.
+
diff --git a/debian/python-rtslib.dirs b/debian/python-rtslib.dirs
new file mode 100644
index 0000000..7a9f9da
--- /dev/null
+++ b/debian/python-rtslib.dirs
@@ -0,0 +1,2 @@
+usr/share/python-support
+var/target/fabric
diff --git a/debian/python-rtslib.docs b/debian/python-rtslib.docs
new file mode 100644
index 0000000..1f562b3
--- /dev/null
+++ b/debian/python-rtslib.docs
@@ -0,0 +1,2 @@
+README
+COPYING
diff --git a/debian/python-rtslib.install b/debian/python-rtslib.install
new file mode 100644
index 0000000..caf0db2
--- /dev/null
+++ b/debian/python-rtslib.install
@@ -0,0 +1,2 @@
+lib/rtslib usr/share/python-support
+specs/* var/target/fabric
diff --git a/debian/python-rtslib.postinst b/debian/python-rtslib.postinst
new file mode 100755
index 0000000..5de7eb9
--- /dev/null
+++ b/debian/python-rtslib.postinst
@@ -0,0 +1,17 @@
+#!/bin/sh
+for lib in lib lib64; do
+ for python in python2.5 python2.6; do
+ if [ -e /usr/${lib}/${python} ]; then
+ if [ ! -e /usr/${lib}/${python}/rtslib ]; then
+ mkdir /usr/${lib}/${python}/rtslib
+ for source in /usr/share/python-support/rtslib/rtslib/*.py; do
+ ln -sf ${source} /usr/${lib}/${python}/rtslib/
+ done
+ python_path=$(which ${python} 2>/dev/null)
+ if [ ! -z $python_path ]; then
+ ${python} -c "import compileall; compileall.compile_dir('/usr/${lib}/${python}/rtslib', force=1)"
+ fi
+ fi
+ fi
+ done
+done
diff --git a/debian/python-rtslib.preinst b/debian/python-rtslib.preinst
new file mode 100755
index 0000000..f4815e6
--- /dev/null
+++ b/debian/python-rtslib.preinst
@@ -0,0 +1,3 @@
+#!/bin/sh
+rm -f /usr/share/python-support/rtslib/rtslib/*.pyc
+rm -f /usr/share/python-support/rtslib/rtslib/*.pyo
diff --git a/debian/python-rtslib.prerm b/debian/python-rtslib.prerm
new file mode 100755
index 0000000..8a089c4
--- /dev/null
+++ b/debian/python-rtslib.prerm
@@ -0,0 +1,8 @@
+#!/bin/sh
+for lib in lib lib64; do
+ for python in python2.5 python2.6; do
+ if [ -e /usr/${lib}/${python}/rtslib ]; then
+ rm -rf /usr/${lib}/${python}/rtslib
+ fi
+ done
+done
diff --git a/debian/pyversions b/debian/pyversions
new file mode 100644
index 0000000..b3dc41e
--- /dev/null
+++ b/debian/pyversions
@@ -0,0 +1 @@
+2.5-
diff --git a/debian/rtslib-doc.docs b/debian/rtslib-doc.docs
new file mode 100644
index 0000000..db4cb0e
--- /dev/null
+++ b/debian/rtslib-doc.docs
@@ -0,0 +1,4 @@
+README
+COPYING
+doc/pdf
+doc/html
diff --git a/debian/rules b/debian/rules
new file mode 100755
index 0000000..6d69065
--- /dev/null
+++ b/debian/rules
@@ -0,0 +1,49 @@
+#!/usr/bin/make -f
+
+build_dir = build
+install_dir = debian/tmp
+setup = /usr/bin/python ./setup.py --quiet
+
+binary: binary-indep
+
+binary-arch:
+
+binary-indep: build install
+ dh_testdir
+ dh_testroot
+ dh_installchangelogs
+ dh_installdocs
+ dh_installman
+ dh_install --list-missing --sourcedir $(install_dir)
+ dh_fixperms
+ dh_compress -X.py
+ dh_installdeb
+ dh_gencontrol
+ dh_md5sums
+ dh_builddeb
+
+install: build
+ dh_testdir
+ dh_testroot
+ dh_installdirs
+ cp -R specs $(install_dir)
+
+build: build-stamp
+build-stamp:
+ dh_testdir
+ $(setup) build --build-base $(build_dir) install --no-compile --install-purelib $(install_dir)/lib/rtslib --install-scripts $(install_dir)/bin
+ echo "2.5, 2.6" > $(install_dir)/lib/rtslib/.version
+ touch build-stamp
+
+clean:
+ dh_testdir
+ dh_testroot
+ rm -f build-stamp
+ $(setup) clean
+ find . -name "*.pyc" | xargs rm -f
+ find . -name "*.pyo" | xargs rm -f
+ rm -rf $(build_dir) $(install_dir)
+ dh_clean
+
+.PHONY: binary binary-indep install build clean
+
diff --git a/rtslib/__init__.py b/rtslib/__init__.py
new file mode 100644
index 0000000..b59eba6
--- /dev/null
+++ b/rtslib/__init__.py
@@ -0,0 +1,35 @@
+'''
+This file is part of RTSLib Community Edition.
+Copyright (c) 2011 by RisingTide Systems LLC
+
+This program is free software: you can redistribute it and/or modify
+it under the terms of the GNU Affero General Public License as
+published by the Free Software Foundation, version 3 (AGPLv3).
+
+This program is distributed in the hope that it will be useful,
+but WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+GNU Affero General Public License for more details.
+
+You should have received a copy of the GNU Affero General Public License
+along with this program. If not, see <http://www.gnu.org/licenses/>.
+'''
+
+import utils
+
+from root import RTSRoot
+from utils import RTSLibError, RTSLibBrokenLink
+
+from target import LUN, MappedLUN
+from target import NodeACL, NetworkPortal, TPG, Target, FabricModule
+
+from tcm import FileIOBackstore, IBlockBackstore
+from tcm import FileIOStorageObject, IBlockStorageObject
+from tcm import PSCSIBackstore, RDDRBackstore, RDMCPBackstore
+from tcm import PSCSIStorageObject, RDDRStorageObject, RDMCPStorageObject
+
+__version__ = 'GIT_VERSION'
+__author__ = "Jerome Martin <jxm@risingtidesystems.com>"
+__url__ = "http://www.risingtidesystems.com"
+__description__ = "API for RisingTide Systems generic SCSI target."
+__license__ = __doc__
diff --git a/rtslib/loop.py b/rtslib/loop.py
new file mode 100644
index 0000000..eb84e84
--- /dev/null
+++ b/rtslib/loop.py
@@ -0,0 +1,465 @@
+'''
+Implements the RTS SAS loopback classes.
+
+This file is part of RTSLib Community Edition.
+Copyright (c) 2011 by RisingTide Systems LLC
+
+This program is free software: you can redistribute it and/or modify
+it under the terms of the GNU Affero General Public License as
+published by the Free Software Foundation, version 3 (AGPLv3).
+
+This program is distributed in the hope that it will be useful,
+but WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+GNU Affero General Public License for more details.
+
+You should have received a copy of the GNU Affero General Public License
+along with this program. If not, see <http://www.gnu.org/licenses/>.
+'''
+
+import re
+import os
+import glob
+import uuid
+import shutil
+
+# rtslib modules
+from root import RTSRoot
+from node import CFSNode
+from utils import RTSLibError, RTSLibBrokenLink
+from utils import generate_wwn, fwrite, fread
+
+class LUN(CFSNode):
+ '''
+ This is an interface to RTS Target LUNs in configFS.
+ A LUN is identified by its parent Nexus and LUN index.
+ '''
+
+ # LUN private stuff
+
+ def __init__(self, parent_nexus, lun, storage_object=None, alias=None):
+ '''
+ A LUN object can be instanciated in two ways:
+ - B{Creation mode}: If I{storage_object} is specified, the
+ underlying configFS object will be created with that parameter.
+ No LUN with the same I{lun} index can pre-exist in the parent
+ Nexus in that mode, or instanciation will fail.
+ - B{Lookup mode}: If I{storage_object} is not set, then the LUN
+ will be bound to the existing configFS LUN object of the parent
+ Nexus having the specified I{lun} index. The underlying configFS
+ object must already exist in that mode.
+
+ @param parent_nexus: The parent Nexus object.
+ @type parent_nexus: Nexus
+ @param lun: The LUN index.
+ @type lun: 0-255
+ @param storage_object: The storage object to be exported as a LUN.
+ @type storage_object: StorageObject subclass
+ @param alias: An optional parameter to manually specify the LUN alias.
+ You probably do not need this.
+ @type alias: string
+ @return: A LUN object.
+ '''
+ super(LUN, self).__init__()
+
+ if isinstance(parent_nexus, Nexus):
+ self._parent_nexus = parent_nexus
+ else:
+ raise RTSLibError("Invalid parent Nexus.")
+
+ try:
+ lun = int(lun)
+ except ValueError:
+ raise RTSLibError("Invalid LUN index: %s" % str(lun))
+ else:
+ if lun > 255 or lun < 0:
+ raise RTSLibError("Invalid LUN index, it must be "
+ + "between 0 and 255: %d" % lun)
+ self._lun = lun
+
+ self._path = "%s/lun/lun_%d" % (self.parent_nexus.path, self.lun)
+
+ if storage_object is None and alias is not None:
+ raise RTSLibError("The alias parameter has no meaning "
+ + "without the storage_object parameter.")
+
+ if storage_object is not None:
+ self._create_in_cfs_ine('create')
+ try:
+ self._configure(storage_object, alias)
+ except:
+ self.delete()
+ raise
+ else:
+ self._create_in_cfs_ine('lookup')
+
+ def __str__(self):
+ try:
+ storage_object = self.storage_object
+ except RTSLibBrokenLink:
+ desc = "[BROKEN STORAGE LINK]"
+ else:
+ backstore = storage_object.backstore
+ soname = storage_object.name
+ if backstore.plugin.startswith("rd"):
+ path = "ramdisk"
+ else:
+ path = storage_object.udev_path
+ desc = "-> %s%d '%s' (%s)" \
+ % (backstore.plugin, backstore.index, soname, path)
+ return "LUN %d %s" % (self.lun, desc)
+
+ def _create_in_cfs_ine(self, mode):
+ super(LUN, self)._create_in_cfs_ine(mode)
+
+ def _configure(self, storage_object, alias):
+ self._check_self()
+ if alias is None:
+ alias = str(uuid.uuid4())[-10:]
+ else:
+ alias = str(alias).strip()
+ if '/' in alias:
+ raise RTSLibError("Invalid alias: %s", alias)
+ destination = "%s/%s" % (self.path, alias)
+ from tcm import StorageObject
+ if isinstance(storage_object, StorageObject):
+ if storage_object.exists:
+ source = storage_object.path
+ else:
+ raise RTSLibError("The storage_object does not exist "
+ + "in configFS.")
+ else:
+ raise RTSLibError("Invalid storage object.")
+
+ os.symlink(source, destination)
+
+ def _get_alias(self):
+ self._check_self()
+ alias = None
+ for path in os.listdir(self.path):
+ if os.path.islink("%s/%s" % (self.path, path)):
+ alias = os.path.basename(path)
+ break
+ if alias is None:
+ raise RTSLibBrokenLink("Broken LUN in configFS, no " \
+ + "storage object attached.")
+ else:
+ return alias
+
+ def _get_storage_object(self):
+ self._check_self()
+ alias_path = None
+ for path in os.listdir(self.path):
+ if os.path.islink("%s/%s" % (self.path, path)):
+ alias_path = os.path.realpath("%s/%s" % (self.path, path))
+ break
+ if alias_path is None:
+ raise RTSLibBrokenLink("Broken LUN in configFS, no " \
+ + "storage object attached.")
+ rtsroot = RTSRoot()
+ for storage_object in rtsroot.storage_objects:
+ if storage_object.path == alias_path:
+ return storage_object
+ raise RTSLibBrokenLink("Broken storage object link in LUN.")
+
+ def _get_parent_nexus(self):
+ return self._parent_nexus
+
+ def _get_lun(self):
+ return self._lun
+
+ def _get_alua_metadata_path(self):
+ return "%s/lun_%d" % (self.parent_nexus.alua_metadata_path, self.lun)
+
+ # LUN public stuff
+
+ def delete(self):
+ '''
+ If the underlying configFS object does not exists, this method does
+ nothing. If the underlying configFS object exists, this method attempts
+ to delete it.
+ '''
+ self._check_self()
+ try:
+ link = self.alias
+ except RTSLibBrokenLink:
+ pass
+ else:
+ if os.path.islink("%s/%s" % (self.path, link)):
+ os.unlink("%s/%s" % (self.path, link))
+
+ super(LUN, self).delete()
+ if os.path.isdir(self.alua_metadata_path):
+ shutil.rmtree(self.alua_metadata_path)
+
+ alua_metadata_path = property(_get_alua_metadata_path,
+ doc="Get the ALUA metadata directory path for the LUN.")
+ parent_nexus = property(_get_parent_nexus,
+ doc="Get the parent Nexus object.")
+ lun = property(_get_lun,
+ doc="Get the LUN index as an int.")
+ storage_object = property(_get_storage_object,
+ doc="Get the storage object attached to the LUN.")
+ alias = property(_get_alias,
+ doc="Get the LUN alias.")
+
+class Nexus(CFSNode):
+ '''
+ This is a an interface to Target Portal Groups in configFS.
+ A Nexus is identified by its parent Target object and its nexus Tag.
+ To a Nexus object is attached a list of NetworkPortals.
+ '''
+
+ # Nexus private stuff
+
+ def __init__(self, parent_target, tag, mode='any'):
+ '''
+ @param parent_target: The parent Target object of the Nexus.
+ @type parent_target: Target
+ @param tag: The Nexus Tag (TPGT).
+ @type tag: int > 0
+ @param mode:An optionnal string containing the object creation mode:
+ - I{'any'} means the configFS object will be either looked up or
+ created.
+ - I{'lookup'} means the object MUST already exist configFS.
+ - I{'create'} means the object must NOT already exist in configFS.
+ @type mode:string
+ @return: A Nexus object.
+ '''
+
+ super(Nexus, self).__init__()
+
+ try:
+ self._tag = int(tag)
+ except ValueError:
+ raise RTSLibError("Invalid Tag.")
+
+ if tag < 1:
+ raise RTSLibError("Invalig Tag, it must be >0.")
+
+ if isinstance(parent_target, Target):
+ self._parent_target = parent_target
+ else:
+ raise RTSLibError("Invalid parent Target.")
+
+ self._path = "%s/tpgt_%d" % (self.parent_target.path, self.tag)
+ self._create_in_cfs_ine(mode)
+
+ def __str__(self):
+ try:
+ initiator = self.initiator
+ except RTSLibError:
+ initiator = "[BROKEN]"
+ return "Nexus %d / initiator %s" % (self.tag, initiator)
+
+ def _get_initiator(self):
+ nexus_path = self._path + "/nexus"
+ if os.path.isfile(nexus_path):
+ try:
+ initiator = fread(nexus_path)
+ except IOError, msg:
+ raise RTSLibError("Cannot read Nexus initiator address "
+ + "(>=4.0 style, %s): %s."
+ % (nexus_path, msg))
+ else:
+ try:
+ initiator = os.listdir(nexus_path)[0]
+ except IOError, msg:
+ raise RTSLibError("Cannot read Nexus initiator address "
+ + "(<4.0 style, %s): %s."
+ % (nexus_path, msg))
+ return initiator.strip()
+
+ def _get_tag(self):
+ return self._tag
+
+ def _get_parent_target(self):
+ return self._parent_target
+
+ def _create_in_cfs_ine(self, mode):
+ super(Nexus, self)._create_in_cfs_ine(mode)
+
+ if not os.path.isdir(self.alua_metadata_path):
+ os.makedirs(self.alua_metadata_path)
+
+ if self._fresh:
+ initiator = generate_wwn('naa')
+ nexus_path = self._path + "/nexus"
+ if os.path.isfile(nexus_path):
+ try:
+ fwrite(nexus_path, initiator)
+ except IOError, msg:
+ raise RTSLibError("Cannot create Nexus initiator "
+ + "(>=4.0 style, %s): %s."
+ % (nexus_path, msg))
+ else:
+ try:
+ os.makedirs(nexus_path + "/" + initiator)
+ except IOError, msg:
+ raise RTSLibError("Cannot create Nexus initiator."
+ + "(<4.0 style, %s): %s."
+ % (nexus_path, msg))
+
+ def _list_luns(self):
+ self._check_self()
+ luns = []
+ lun_dirs = [os.path.basename(path)
+ for path in os.listdir("%s/lun" % self.path)]
+ for lun_dir in lun_dirs:
+ lun = lun_dir.split('_')[1]
+ lun = int(lun)
+ luns.append(LUN(self, lun))
+ return luns
+
+ def _control(self, command):
+ self._check_self()
+ path = "%s/control" % self.path
+ fwrite(path, "%s\n" % str(command))
+
+ def _get_alua_metadata_path(self):
+ return "%s/%s+%d" \
+ % (self.alua_metadata_dir,
+ self.parent_target.naa, self.tag)
+
+ # Nexus public stuff
+
+ def delete(self):
+ '''
+ Recursively deletes a Nexus object.
+ This will delete all attached LUN, and then the Nexus itself.
+ '''
+ self._check_self()
+ for lun in self.luns:
+ lun.delete()
+
+ # TODO: check that ALUA MD removal works while removing Nexus
+ if os.path.isdir(self.alua_metadata_path):
+ shutil.rmtree(self.alua_metadata_path)
+
+ nexus_path = self._path + "/nexus"
+ if os.path.isfile(nexus_path):
+ try:
+ fwrite(nexus_path, "NULL")
+ except IOError, msg:
+ raise RTSLibError("Cannot delete Nexus initiator "
+ + "(>=4.0 style, %s): %s."
+ % (nexus_path, msg))
+ else:
+ try:
+ os.rmdir(nexus_path + "/" + self.initiator)
+ except IOError, msg:
+ raise RTSLibError("Cannot delete Nexus initiator."
+ + "(<4.0 style, %s): %s."
+ % (nexus_path, msg))
+
+ super(Nexus, self).delete()
+
+ def lun(self, lun, storage_object=None, alias=None):
+ '''
+ Same as LUN() but without specifying the parent_nexus.
+ '''
+ self._check_self()
+ return LUN(self, lun=lun, storage_object=storage_object, alias=alias)
+
+ alua_metadata_path = property(_get_alua_metadata_path,
+ doc="Get the ALUA metadata directory path " \
+ + "for the Nexus.")
+ tag = property(_get_tag,
+ doc="Get the Nexus Tag as an int.")
+ initiator = property(_get_initiator,
+ doc="Get the Nexus initiator address as a string.")
+ parent_target = property(_get_parent_target,
+ doc="Get the parent Target object to which the " \
+ + "Nexus is attached.")
+ luns = property(_list_luns,
+ doc="Get the list of LUN objects currently attached " \
+ + "to the Nexus.")
+
+class Target(CFSNode):
+ '''
+ This is an interface to loopback SAS Targets in configFS.
+ A Target is identified by its naa SAS address.
+ To a Target is attached a list of Nexus objects.
+ '''
+
+ # Target private stuff
+
+ def __init__(self, naa=None, mode='any'):
+ '''
+ @param naa: The optionnal Target's address.
+ If no address or an empty address is specified, one will be
+ generated for you.
+ @type naa: string
+ @param mode:An optionnal string containing the object creation mode:
+ - I{'any'} means the configFS object will be either looked up
+ or created.
+ - I{'lookup'} means the object MUST already exist configFS.
+ - I{'create'} means the object must NOT already exist in configFS.
+ @type mode:string
+ @return: A Target object.
+ '''
+
+ super(Target, self).__init__()
+
+ if naa is None:
+ naa = generate_wwn('naa')
+ else:
+ naa = str(naa).lower().strip()
+ self._naa = naa
+ self._path = "%s/loopback/%s" % (self.configfs_dir, self._naa)
+ if not self:
+ if not re.match(
+ "naa\.[0-9]+", naa) \
+ or re.search(' ', naa) \
+ or re.search('_', naa):
+ raise RTSLibError("Invalid naa: %s"
+ % naa)
+ self._create_in_cfs_ine(mode)
+
+ def __str__(self):
+ return "SAS loopback %s" % self.naa
+
+ def _list_nexuses(self):
+ self._check_self()
+ nexuses = []
+ nexus_dirs = glob.glob("%s/tpgt*" % self.path)
+ for nexus_dir in nexus_dirs:
+ tag = os.path.basename(nexus_dir).split('_')[1]
+ tag = int(tag)
+ nexuses.append(Nexus(self, tag, 'lookup'))
+ return nexuses
+
+ def _get_naa(self):
+ return self._naa
+
+ # Target public stuff
+
+ def delete(self):
+ '''
+ Recursively deletes a Target object.
+ This will delete all attached Nexus objects and then the Target itself.
+ '''
+ self._check_self()
+ for nexus in self.nexuses:
+ nexus.delete()
+ super(Target, self).delete()
+
+ def nexus(self, tag, mode='any'):
+ '''
+ Same as Nexus() but without the parent_target parameter.
+ '''
+ self._check_self()
+ return Nexus(self, tag=tag, mode=mode)
+
+ naa = property(_get_naa,
+ doc="Get the naa of the Target object as a string.")
+ nexuses = property(_list_nexuses,
+ doc="Get the list of Nexus objects currently "
+ + "attached to the Target.")
+
+def _test():
+ import doctest
+ doctest.testmod()
+
+if __name__ == "__main__":
+ _test()
diff --git a/rtslib/node.py b/rtslib/node.py
new file mode 100644
index 0000000..f688723
--- /dev/null
+++ b/rtslib/node.py
@@ -0,0 +1,236 @@
+'''
+Implements the base CFSNode class and a few inherited variants.
+
+This file is part of RTSLib Community Edition.
+Copyright (c) 2011 by RisingTide Systems LLC
+
+This program is free software: you can redistribute it and/or modify
+it under the terms of the GNU Affero General Public License as
+published by the Free Software Foundation, version 3 (AGPLv3).
+
+This program is distributed in the hope that it will be useful,
+but WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+GNU Affero General Public License for more details.
+
+You should have received a copy of the GNU Affero General Public License
+along with this program. If not, see <http://www.gnu.org/licenses/>.
+'''
+
+import os
+import stat
+from utils import fread, fwrite, RTSLibError
+
+class CFSNode(object):
+
+ # Where do we store the fabric modules spec files ?
+ spec_dir = "/var/target/fabric"
+ # Where is the configfs base LIO directory ?
+ configfs_dir = '/sys/kernel/config/target'
+ # TODO: Make the ALUA path generic, not iscsi-centric
+ # What is the ALUA directory ?
+ alua_metadata_dir = "/var/target/alua/iSCSI"
+
+ # CFSNode private stuff
+
+ def __init__(self):
+ self._path = self.configfs_dir
+
+ def __nonzero__(self):
+ if os.path.isdir(self.path):
+ return True
+ else:
+ return False
+
+ def __str__(self):
+ return self.path
+
+ def _get_path(self):
+ return self._path
+
+ def _create_in_cfs_ine(self, mode):
+ '''
+ Creates the configFS node if it does not already exists depending on
+ the mode.
+ any -> makes sure it exists, also works if the node already does exists
+ lookup -> make sure it does NOT exists
+ create -> create the node which must not exists beforehand
+ Upon success (no exception raised), self._fresh is True if a node was
+ created, else self._fresh is False.
+ '''
+ if mode not in ['any', 'lookup', 'create']:
+ raise RTSLibError("Invalid mode: %s" % mode)
+ if self and mode == 'create':
+ raise RTSLibError("This %s already exists in configFS."
+ % self.__class__.__name__)
+ elif not self and mode == 'lookup':
+ raise RTSLibError("No such %s in configfs: %s."
+ % (self.__class__.__name__, self.path))
+ if not self:
+ os.mkdir(self.path)
+ self._fresh = True
+ else:
+ self._fresh = False
+
+ def _exists(self):
+ return bool(self)
+
+ def _check_self(self):
+ if not self:
+ raise RTSLibError("This %s does not exist in configFS."
+ % self.__class__.__name__)
+
+ def _is_fresh(self):
+ return self._fresh
+
+ def _list_files(self, path, writable=None):
+ '''
+ List files under a path depending on their owner's write permissions.
+ @param path: The path under which the files are expected to be. If the
+ path itself is not a directory, an empty list will be returned.
+ @type path: str
+ @param writable: If None (default), returns all parameters, if True,
+ returns read-write parameters, if False, returns just the read-only
+ parameters.
+ @type writable: bool or None
+ @return: List of file names filtered according to their write perms.
+ '''
+ if not os.path.isdir(path):
+ return []
+
+ if writable is None:
+ names = os.listdir(path)
+ elif writable:
+ names = [name for name in os.listdir(path)
+ if (os.stat("%s/%s" % (path, name))[stat.ST_MODE] \
+ & stat.S_IWUSR)]
+ else:
+ names = [os.path.basename(name) for name in os.listdir(path)
+ if not (os.stat("%s/%s" % (path, name))[stat.ST_MODE] \
+ & stat.S_IWUSR)]
+ names.sort()
+ return names
+
+ # CFSNode public stuff
+
+ def list_parameters(self, writable=None):
+ '''
+ @param writable: If None (default), returns all parameters, if True,
+ returns read-write parameters, if False, returns just the read-only
+ parameters.
+ @type writable: bool or None
+ @return: The list of existing RFC-3720 parameter names.
+ '''
+ self._check_self()
+ path = "%s/param" % self.path
+ return self._list_files(path, writable)
+
+ def list_attributes(self, writable=None):
+ '''
+ @param writable: If None (default), returns all attributes, if True,
+ returns read-write attributes, if False, returns just the read-only
+ attributes.
+ @type writable: bool or None
+ @return: A list of existing attribute names as strings.
+ '''
+ self._check_self()
+ path = "%s/attrib" % self.path
+ return self._list_files(path, writable)
+
+ def set_attribute(self, attribute, value):
+ '''
+ Sets the value of a named attribute.
+ The attribute must exist in configFS.
+ @param attribute: The attribute's name. It is case-sensitive.
+ @type attribute: string
+ @param value: The attribute's value.
+ @type value: string
+ '''
+ self._check_self()
+ path = "%s/attrib/%s" % (self.path, str(attribute))
+ if not os.path.isfile(path):
+ raise RTSLibError("Cannot find attribute: %s."
+ % str(attribute))
+ else:
+ try:
+ fwrite(path, "%s\n" % str(value))
+ except IOError:
+ raise RTSLibError("Cannot set attribute %s."
+ % str(attribute))
+
+ def get_attribute(self, attribute):
+ '''
+ @param attribute: The attribute's name. It is case-sensitive.
+ @return: The named attribute's value, as a string.
+ '''
+ self._check_self()
+ path = "%s/attrib/%s" % (self.path, str(attribute))
+ if not os.path.isfile(path):
+ raise RTSLibError("Cannot find attribute: %s."
+ % str(attribute))
+ else:
+ return fread(path).strip()
+
+ def set_parameter(self, parameter, value):
+ '''
+ Sets the value of a named RFC-3720 parameter.
+ The parameter must exist in configFS.
+ @param parameter: The RFC-3720 parameter's name. It is case-sensitive.
+ @type parameter: string
+ @param value: The parameter's value.
+ @type value: string
+ '''
+ self._check_self()
+ path = "%s/param/%s" % (self.path, str(parameter))
+ if not os.path.isfile(path):
+ raise RTSLibError("Cannot find parameter: %s."
+ % str(parameter))
+ else:
+ try:
+ fwrite(path, "%s \n" % str(value))
+ except IOError:
+ raise RTSLibError("Cannot set parameter %s."
+ % str(parameter))
+
+ def get_parameter(self, parameter):
+ '''
+ @param parameter: The RFC-3720 parameter's name. It is case-sensitive.
+ @type parameter: string
+ @return: The named parameter value as a string.
+ '''
+ self._check_self()
+ path = "%s/param/%s" % (self.path, str(parameter))
+ if not os.path.isfile(path):
+ raise RTSLibError("Cannot find RFC-3720 parameter: %s."
+ % str(parameter))
+ else:
+ return fread(path).rstrip()
+
+ def delete(self):
+ '''
+ If the underlying configFS object does not exists, this method does
+ nothing. If the underlying configFS object exists, this method attempts
+ to delete it.
+ '''
+ if self:
+ os.rmdir(self.path)
+
+ path = property(_get_path,
+ doc="Get the configFS object path.")
+ exists = property(_exists,
+ doc="Is True as long as the underlying configFS object exists. " \
+ + "If the underlying configFS objects gets deleted " \
+ + "either by calling the delete() method, or by any " \
+ + "other means, it will be False.")
+ is_fresh = property(_is_fresh,
+ doc="Is True if the underlying configFS object has been created " \
+ + "when instanciating this particular object. Is " \
+ + "False if this object instanciation just looked " \
+ + "up the underlying configFS object.")
+
+def _test():
+ import doctest
+ doctest.testmod()
+
+if __name__ == "__main__":
+ _test()
diff --git a/rtslib/root.py b/rtslib/root.py
new file mode 100644
index 0000000..3ea89da
--- /dev/null
+++ b/rtslib/root.py
@@ -0,0 +1,164 @@
+'''
+Implements the RTSRoot class.
+
+This file is part of RTSLib Community Edition.
+Copyright (c) 2011 by RisingTide Systems LLC
+
+This program is free software: you can redistribute it and/or modify
+it under the terms of the GNU Affero General Public License as
+published by the Free Software Foundation, version 3 (AGPLv3).
+
+This program is distributed in the hope that it will be useful,
+but WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+GNU Affero General Public License for more details.
+
+You should have received a copy of the GNU Affero General Public License
+along with this program. If not, see <http://www.gnu.org/licenses/>.
+'''
+
+import re
+import os
+import glob
+
+from node import CFSNode
+from target import Target, FabricModule
+from tcm import FileIOBackstore, IBlockBackstore
+from tcm import PSCSIBackstore, RDDRBackstore, RDMCPBackstore
+from utils import RTSLibError, RTSLibBrokenLink, flatten_nested_list, modprobe
+
+class RTSRoot(CFSNode):
+ '''
+ This is an interface to the root of the configFS object tree.
+ Is allows one to start browsing Target and Backstore objects,
+ as well as helper methods to return arbitrary objects from the
+ configFS tree.
+
+ >>> import rtslib.root as root
+ >>> rtsroot = root.RTSRoot()
+ >>> rtsroot.path
+ '/sys/kernel/config/target'
+ >>> rtsroot.exists
+ True
+ >>> rtsroot.targets # doctest: +ELLIPSIS
+ [...]
+ >>> rtsroot.backstores # doctest: +ELLIPSIS
+ [...]
+ >>> rtsroot.tpgs # doctest: +ELLIPSIS
+ [...]
+ >>> rtsroot.storage_objects # doctest: +ELLIPSIS
+ [...]
+ >>> rtsroot.network_portals # doctest: +ELLIPSIS
+ [...]
+
+ '''
+
+ # The core target/tcm kernel module
+ target_core_mod = 'target_core_mod'
+
+ # RTSRoot private stuff
+ def __init__(self):
+ '''
+ Instanciate an RTSRoot object. Basically checks for configfs setup and
+ base kernel modules (tcm )
+ '''
+ super(RTSRoot, self).__init__()
+ modprobe(self.target_core_mod)
+ self._create_in_cfs_ine('any')
+
+ def _list_targets(self):
+ self._check_self()
+ targets = set([])
+ for fabric_module in self.fabric_modules:
+ targets.update(fabric_module.targets)
+ return targets
+
+ def _list_backstores(self):
+ self._check_self()
+ backstores = set([])
+ if os.path.isdir("%s/core" % self.path):
+ backstore_dirs = glob.glob("%s/core/*_*" % self.path)
+ for backstore_dir in [os.path.basename(path)
+ for path in backstore_dirs]:
+ regex = re.search("([a-z]+[_]*[a-z]+)(_)([0-9]+)",
+ backstore_dir)
+ if regex:
+ if regex.group(1) == "fileio":
+ backstores.add(
+ FileIOBackstore(int(regex.group(3)), 'lookup'))
+ elif regex.group(1) == "pscsi":
+ backstores.add(
+ PSCSIBackstore(int(regex.group(3)), 'lookup'))
+ elif regex.group(1) == "iblock":
+ backstores.add(
+ IBlockBackstore(int(regex.group(3)), 'lookup'))
+ elif regex.group(1) == "rd_dr":
+ backstores.add(
+ RDDRBackstore(int(regex.group(3)), 'lookup'))
+ elif regex.group(1) == "rd_mcp":
+ backstores.add(
+ RDMCPBackstore(int(regex.group(3)), 'lookup'))
+ return backstores
+
+ def _list_storage_objects(self):
+ self._check_self()
+ return set(flatten_nested_list([backstore.storage_objects
+ for backstore in self.backstores]))
+
+ def _list_tpgs(self):
+ self._check_self()
+ return set(flatten_nested_list([t.tpgs for t in self.targets]))
+
+ def _list_node_acls(self):
+ self._check_self()
+ return set(flatten_nested_list([t.node_acls for t in self.tpgs]))
+
+ def _list_network_portals(self):
+ self._check_self()
+ return set(flatten_nested_list([t.network_portals for t in self.tpgs]))
+
+ def _list_luns(self):
+ self._check_self()
+ return set(flatten_nested_list([t.luns for t in self.tpgs]))
+
+ def _list_fabric_modules(self):
+ self._check_self()
+ mod_names = [mod_name[:-5] for mod_name in os.listdir(self.spec_dir)
+ if mod_name.endswith('.spec')]
+ modules = [FabricModule(mod_name) for mod_name in mod_names]
+ return modules
+
+ def _list_loaded_fabric_modules(self):
+ return [fm for fm in self._list_fabric_modules() if fm.exists]
+
+ def __str__(self):
+ return "rtsadmin"
+
+ # RTSRoot public stuff
+
+ backstores = property(_list_backstores,
+ doc="Get the list of Backstore objects.")
+ targets = property(_list_targets,
+ doc="Get the list of Target objects.")
+ tpgs = property(_list_tpgs,
+ doc="Get the list of all the existing TPG objects.")
+ node_acls = property(_list_node_acls,
+ doc="Get the list of all the existing NodeACL objects.")
+ network_portals = property(_list_network_portals,
+ doc="Get the list of all the existing Network Portal objects.")
+ storage_objects = property(_list_storage_objects,
+ doc="Get the list of all the existing Storage objects.")
+ luns = property(_list_luns,
+ doc="Get the list of all existing LUN objects.")
+ fabric_modules = property(_list_fabric_modules,
+ doc="Get the list of all FabricModule objects.")
+ loaded_fabric_modules = property(_list_loaded_fabric_modules,
+ doc="Get the list of all loaded FabricModule objects.")
+
+def _test():
+ '''Run the doctests.'''
+ import doctest
+ doctest.testmod()
+
+if __name__ == "__main__":
+ _test()
diff --git a/rtslib/target.py b/rtslib/target.py
new file mode 100644
index 0000000..1c1d16a
--- /dev/null
+++ b/rtslib/target.py
@@ -0,0 +1,1157 @@
+'''
+Implements the RTS generic Target fabric classes.
+
+This file is part of RTSLib Community Edition.
+Copyright (c) 2011 by RisingTide Systems LLC
+
+This program is free software: you can redistribute it and/or modify
+it under the terms of the GNU Affero General Public License as
+published by the Free Software Foundation, version 3 (AGPLv3).
+
+This program is distributed in the hope that it will be useful,
+but WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+GNU Affero General Public License for more details.
+
+You should have received a copy of the GNU Affero General Public License
+along with this program. If not, see <http://www.gnu.org/licenses/>.
+'''
+
+import re
+import os
+import glob
+import uuid
+import shutil
+
+from node import CFSNode
+from utils import RTSLibError, RTSLibBrokenLink, modprobe
+from utils import fread, fwrite, generate_wwn, is_valid_wwn, exec_argv
+
+class FabricModule(CFSNode):
+ '''
+ This is an interface to RTS Target Fabric Modules.
+ It can load/unload modules, provide information about them and
+ handle the configfs housekeeping. It uses module configuration
+ files in /var/target/fabric/*.spec. After instanciation, whether or
+ not the fabric module is loaded and
+ '''
+
+ version_attributes = set(["lio_version", "version"])
+ discovery_auth_attributes = set(["discovery_auth"])
+ target_names_excludes = version_attributes | discovery_auth_attributes
+
+ # FabricModule private stuff
+ def __init__(self, name):
+ '''
+ Instanciate a FabricModule object, according to the provided name.
+ @param name: the name of the FabricModule object. It must match an
+ existing target fabric module specfile (name.spec).
+ @type name: str
+ '''
+ super(FabricModule, self).__init__()
+ self.name = name
+ self.spec = self._parse_spec()
+ self._path = "%s/%s" % (self.configfs_dir,
+ self.spec['configfs_group'])
+ # FabricModule public stuff
+
+ def has_feature(self, feature):
+ '''
+ Whether or not this FabricModule has a certain feature.
+ '''
+ if feature in self.spec['features']:
+ return True
+ else:
+ return False
+
+ def load(self, yield_steps=False):
+ '''
+ Attempt to load the target fabric kernel module as defined in the
+ specfile.
+ @param yield_steps: Whether or not to yield an (action, taken, desc)
+ tuple at each step: action is either 'load_module' or
+ 'create_cfs_group', 'taken' is a bool indicating whether the action was
+ taken (if needed) or not, and desc is a text description of the step
+ suitable for logging.
+ @type yield_steps: bool
+ @raises RTSLibError: For failure to load kernel module and/or create
+ configfs group.
+ '''
+ module = self.spec['kernel_module']
+ load_module = modprobe(module)
+ if yield_steps:
+ yield ('load_module', load_module,
+ "Loaded %s kernel module." % module)
+
+ # TODO: Also load saved targets and config if needed. For that, support
+ # XXX: from the configfs side would be nice: have a config ID present
+ # XXX: both on the on-disk saved config and a configfs attibute.
+
+ # Create the configfs group
+ self._create_in_cfs_ine('any')
+ if yield_steps:
+ yield ('create_cfs_group', self._fresh,
+ "Created '%s'." % self.path)
+
+ def _parse_spec(self):
+ '''
+ Parses the fabric module spec file.
+ '''
+ import configobj
+
+ # Recognized options and their default values
+ defaults = dict(features=['discovery_auth', 'acls', 'acls_auth', 'nps',
+ 'tpgts'],
+ kernel_module="%s_target_mod" % self.name,
+ configfs_group=self.name,
+ wwn_from_files=[],
+ wwn_from_files_filter='',
+ wwn_from_cmds=[],
+ wwn_from_cmds_filter='',
+ wwn_type='free')
+
+ spec_file = "%s/%s.spec" % (self.spec_dir, self.name)
+ spec = configobj.ConfigObj(spec_file).dict()
+ if spec:
+ self.spec_file = spec_file
+ else:
+ self.spec_file = ''
+
+ # Do not allow unknown options
+ unknown_options = set(spec.keys()) - set(defaults.keys())
+ if unknown_options:
+ raise RTSLibError("Unknown option(s) in %s: %s"
+ % (spec_file, list(unknown_options)))
+
+ # Use defaults for missing options
+ missing_options = set(defaults.keys()) - set(spec.keys())
+ for option in missing_options:
+ spec[option] = defaults[option]
+
+ # Type conversion and checking
+ for option in spec:
+ spec_type = type(spec[option]).__name__
+ defaults_type = type(defaults[option]).__name__
+ if spec_type != defaults_type:
+ # Type mismatch, go through acceptable conversions
+ if spec_type == 'str' and defaults_type == 'list':
+ spec[option] = [spec[option]]
+ else:
+ raise RTSLibError("Wrong type for option '%s' in %s. "
+ % (option, spec_file)
+ + "Expected type '%s' and got '%s'."
+ % (defaults_type, spec_type))
+
+ # Generate the list of fixed WWNs if not empty
+ wwn_list = None
+ wwn_type = spec['wwn_type']
+
+ if spec['wwn_from_files']:
+ for wwn_pattern in spec['wwn_from_files']:
+ for wwn_file in glob.iglob(wwn_pattern):
+ wwns_in_file = re.split('\t|\0|\n| ', fread(wwn_file))
+ if spec['wwn_from_files_filter']:
+ wwns_filtered = []
+ for wwn in wwns_in_file:
+ filter = "echo %s|%s" \
+ % (wwn, spec['wwn_from_files_filter'])
+ wwns_filtered.append(exec_argv(filter, shell=True))
+ else:
+ wwns_filtered = wwns_in_file
+
+ if wwn_list is None:
+ wwn_list = set([])
+ wwn_list.update(set([wwn for wwn in wwns_filtered
+ if is_valid_wwn(wwn_type, wwn)
+ if wwn]
+ ))
+ if spec['wwn_from_cmds']:
+ for wwn_cmd in spec['wwn_from_cmds']:
+ cmd_result = exec_argv(wwn_cmd, shell=True)
+ wwns_from_cmd = re.split('\t|\0|\n| ', cmd_result)
+ if spec['wwn_from_cmds_filter']:
+ wwns_filtered = []
+ for wwn in wwns_from_cmd:
+ filter = "echo %s|%s" \
+ % (wwn, spec['wwn_from_cmds_filter'])
+ wwns_filtered.append(exec_argv(filter, shell=True))
+ else:
+ wwns_filtered = wwns_from_cmd
+
+ if wwn_list is None:
+ wwn_list = set([])
+ wwn_list.update(set([wwn for wwn in wwns_filtered
+ if is_valid_wwn(wwn_type, wwn)
+ if wwn]
+ ))
+
+ spec['wwn_list'] = wwn_list
+ return spec
+
+ def _list_targets(self):
+ if self.exists:
+ return set(
+ [Target(self, wwn, 'lookup')
+ for wwn in os.listdir(self.path)
+ if os.path.isdir("%s/%s" % (self.path, wwn))
+ if wwn not in self.target_names_excludes])
+ else:
+ return set([])
+
+ def _get_version(self):
+ if self.exists:
+ for attr in self.version_attributes:
+ path = "%s/%s" % (self.path, attr)
+ if os.path.isfile(path):
+ return fread(path)
+ else:
+ raise RTSLibError("Can't find version for fabric module %s."
+ % self.name)
+ else:
+ return None
+
+ # FabricModule public stuff
+
+ def is_valid_wwn(self, wwn):
+ '''
+ Checks whether or not the provided WWN is valid for this fabric module
+ according to the spec file.
+ '''
+ return is_valid_wwn(self.spec['wwn_type'], wwn, self.spec['wwn_list'])
+
+ targets = property(_list_targets,
+ doc="Get the list of target objects.")
+
+ version = property(_get_version,
+ doc="Get the fabric module version string.")
+
+class LUN(CFSNode):
+ '''
+ This is an interface to RTS Target LUNs in configFS.
+ A LUN is identified by its parent TPG and LUN index.
+ '''
+
+ # LUN private stuff
+
+ def __init__(self, parent_tpg, lun, storage_object=None, alias=None):
+ '''
+ A LUN object can be instanciated in two ways:
+ - B{Creation mode}: If I{storage_object} is specified, the
+ underlying configFS object will be created with that parameter.
+ No LUN with the same I{lun} index can pre-exist in the parent TPG
+ in that mode, or instanciation will fail.
+ - B{Lookup mode}: If I{storage_object} is not set, then the LUN
+ will be bound to the existing configFS LUN object of the parent
+ TPG having the specified I{lun} index. The underlying configFS
+ object must already exist in that mode.
+
+ @param parent_tpg: The parent TPG object.
+ @type parent_tpg: TPG
+ @param lun: The LUN index.
+ @type lun: 0-255
+ @param storage_object: The storage object to be exported as a LUN.
+ @type storage_object: StorageObject subclass
+ @param alias: An optional parameter to manually specify the LUN alias.
+ You probably do not need this.
+ @type alias: string
+ @return: A LUN object.
+ '''
+ super(LUN, self).__init__()
+
+ if isinstance(parent_tpg, TPG):
+ self._parent_tpg = parent_tpg
+ else:
+ raise RTSLibError("Invalid parent TPG.")
+
+ try:
+ lun = int(lun)
+ except ValueError:
+ raise RTSLibError("Invalid LUN index: %s" % str(lun))
+ else:
+ if lun > 255 or lun < 0:
+ raise RTSLibError("Invalid LUN index, it must be " \
+ + "between 0 and 255: %d" % lun)
+ self._lun = lun
+
+ self._path = "%s/lun/lun_%d" % (self.parent_tpg.path, self.lun)
+
+ if storage_object is None and alias is not None:
+ raise RTSLibError("The alias parameter has no meaning " \
+ + "without the storage_object parameter.")
+
+ if storage_object is not None:
+ self._create_in_cfs_ine('create')
+ try:
+ self._configure(storage_object, alias)
+ except:
+ self.delete()
+ raise
+ else:
+ self._create_in_cfs_ine('lookup')
+
+ def _create_in_cfs_ine(self, mode):
+ super(LUN, self)._create_in_cfs_ine(mode)
+
+ def _configure(self, storage_object, alias):
+ self._check_self()
+ if alias is None:
+ alias = str(uuid.uuid4())[-10:]
+ else:
+ alias = str(alias).strip()
+ if '/' in alias:
+ raise RTSLibError("Invalid alias: %s", alias)
+ destination = "%s/%s" % (self.path, alias)
+ from tcm import StorageObject
+ if isinstance(storage_object, StorageObject):
+ if storage_object.exists:
+ source = storage_object.path
+ else:
+ raise RTSLibError("The storage_object does not exist " \
+ + "in configFS.")
+ else:
+ raise RTSLibError("Invalid storage object.")
+
+ os.symlink(source, destination)
+
+ def _get_alias(self):
+ self._check_self()
+ alias = None
+ for path in os.listdir(self.path):
+ if os.path.islink("%s/%s" % (self.path, path)):
+ alias = os.path.basename(path)
+ break
+ if alias is None:
+ raise RTSLibBrokenLink("Broken LUN in configFS, no " \
+ + "storage object attached.")
+ else:
+ return alias
+
+ def _get_storage_object(self):
+ self._check_self()
+ alias_path = None
+ for path in os.listdir(self.path):
+ if os.path.islink("%s/%s" % (self.path, path)):
+ alias_path = os.path.realpath("%s/%s" % (self.path, path))
+ break
+ if alias_path is None:
+ raise RTSLibBrokenLink("Broken LUN in configFS, no "
+ + "storage object attached.")
+ from root import RTSRoot
+ rtsroot = RTSRoot()
+ for storage_object in rtsroot.storage_objects:
+ if storage_object.path == alias_path:
+ return storage_object
+ raise RTSLibBrokenLink("Broken storage object link in LUN.")
+
+ def _get_parent_tpg(self):
+ return self._parent_tpg
+
+ def _get_lun(self):
+ return self._lun
+
+ def _get_alua_metadata_path(self):
+ return "%s/lun_%d" % (self.parent_tpg.alua_metadata_path, self.lun)
+
+ def _list_mapped_luns(self):
+ self._check_self()
+ listdir = os.listdir
+ realpath = os.path.realpath
+ path = self.path
+
+ tpg = self.parent_tpg
+ if not tpg.has_feature('acls'):
+ return []
+ else:
+ base = "%s/acls/" % tpg.path
+ xmlun = ["param", "info", "cmdsn_depth", "auth", "attrib"]
+ return [MappedLUN(NodeACL(tpg, nodeacl), mapped_lun.split('_')[1])
+ for nodeacl in listdir(base)
+ for mapped_lun in listdir("%s/%s" % (base, nodeacl))
+ if mapped_lun not in xmlun
+ for link in listdir("%s/%s/%s" \
+ % (base, nodeacl, mapped_lun))
+ if realpath("%s/%s/%s/%s" \
+ % (base, nodeacl, mapped_lun, link)) == path]
+
+ # LUN public stuff
+
+ def delete(self):
+ '''
+ If the underlying configFS object does not exists, this method does
+ nothing. If the underlying configFS object exists, this method attempts
+ to delete it along with all MappedLUN objects referencing that LUN.
+ '''
+ self._check_self()
+ [mlun.delete() for mlun in self._list_mapped_luns()]
+ try:
+ link = self.alias
+ except RTSLibBrokenLink:
+ pass
+ else:
+ if os.path.islink("%s/%s" % (self.path, link)):
+ os.unlink("%s/%s" % (self.path, link))
+
+ super(LUN, self).delete()
+ if os.path.isdir(self.alua_metadata_path):
+ shutil.rmtree(self.alua_metadata_path)
+
+ alua_metadata_path = property(_get_alua_metadata_path,
+ doc="Get the ALUA metadata directory path for the LUN.")
+ parent_tpg = property(_get_parent_tpg,
+ doc="Get the parent TPG object.")
+ lun = property(_get_lun,
+ doc="Get the LUN index as an int.")
+ storage_object = property(_get_storage_object,
+ doc="Get the storage object attached to the LUN.")
+ alias = property(_get_alias,
+ doc="Get the LUN alias.")
+ mapped_luns = property(_list_mapped_luns,
+ doc="List all MappedLUN objects referencing this LUN.")
+
+class MappedLUN(CFSNode):
+ '''
+ This is an interface to RTS Target Mapped LUNs.
+ A MappedLUN is a mapping of a TPG LUN to a specific initiator node, and is
+ part of a NodeACL. It allows the initiator to actually access the TPG LUN
+ if ACLs are enabled for the TPG. The initial TPG LUN will then be seen by
+ the initiator node as the MappedLUN.
+ '''
+
+ # MappedLUN private stuff
+
+ def __init__(self, parent_nodeacl, mapped_lun,
+ tpg_lun=None, write_protect=None):
+ '''
+ A MappedLUN object can be instanciated in two ways:
+ - B{Creation mode}: If I{tpg_lun} is specified, the underlying
+ configFS object will be created with that parameter. No MappedLUN
+ with the same I{mapped_lun} index can pre-exist in the parent
+ NodeACL in that mode, or instanciation will fail.
+ - B{Lookup mode}: If I{tpg_lun} is not set, then the MappedLUN will
+ be bound to the existing configFS MappedLUN object of the parent
+ NodeACL having the specified I{mapped_lun} index. The underlying
+ configFS object must already exist in that mode.
+
+ @param mapped_lun: The mapped LUN index.
+ @type mapped_lun: int
+ @param tpg_lun: The TPG LUN index to map, or directly a LUN object that
+ belong to the same TPG as the
+ parent NodeACL.
+ @type tpg_lun: int or LUN
+ @param write_protect: The write-protect flag value, defaults to False
+ (write-protection disabled).
+ @type write_protect: bool
+ '''
+
+ super(MappedLUN, self).__init__()
+
+ if not isinstance(parent_nodeacl, NodeACL):
+ raise RTSLibError("The parent_nodeacl parameter must be " \
+ + "a NodeACL object.")
+ else:
+ self._parent_nodeacl = parent_nodeacl
+ if not parent_nodeacl.exists:
+ raise RTSLibError("The parent_nodeacl does not exist.")
+
+ try:
+ self._mapped_lun = int(mapped_lun)
+ except ValueError:
+ raise RTSLibError("The mapped_lun parameter must be an " \
+ + "integer value.")
+
+ self._path = "%s/lun_%d" % (self.parent_nodeacl.path, self.mapped_lun)
+
+ if tpg_lun is None and write_protect is not None:
+ raise RTSLibError("The write_protect parameter has no " \
+ + "meaning without the tpg_lun parameter.")
+
+ if tpg_lun is not None:
+ self._create_in_cfs_ine('create')
+ try:
+ self._configure(tpg_lun, write_protect)
+ except:
+ self.delete()
+ raise
+ else:
+ self._create_in_cfs_ine('lookup')
+
+ def _configure(self, tpg_lun, write_protect):
+ self._check_self()
+ if isinstance(tpg_lun, LUN):
+ tpg_lun = tpg_lun.lun
+ else:
+ try:
+ tpg_lun = int(tpg_lun)
+ except ValueError:
+ raise RTSLibError("The tpg_lun must be either an "
+ + "integer or a LUN object.")
+ # Check that the tpg_lun exists in the TPG
+ for lun in self.parent_nodeacl.parent_tpg.luns:
+ if lun.lun == tpg_lun:
+ tpg_lun = lun
+ break
+ if not (isinstance(tpg_lun, LUN) and tpg_lun):
+ raise RTSLibError("LUN %s does not exist in this TPG."
+ % str(tpg_lun))
+ os.symlink(tpg_lun.path, "%s/%s"
+ % (self.path, str(uuid.uuid4())[-10:]))
+ if write_protect:
+ self.write_protect = True
+ else:
+ self.write_protect = False
+
+ def _get_alias(self):
+ self._check_self()
+ alias = None
+ for path in os.listdir(self.path):
+ if os.path.islink("%s/%s" % (self.path, path)):
+ alias = os.path.basename(path)
+ break
+ if alias is None:
+ raise RTSLibBrokenLink("Broken LUN in configFS, no " \
+ + "storage object attached.")
+ else:
+ return alias
+
+ def _get_mapped_lun(self):
+ return self._mapped_lun
+
+ def _get_parent_nodeacl(self):
+ return self._parent_nodeacl
+
+ def _set_write_protect(self, write_protect):
+ self._check_self()
+ path = "%s/write_protect" % self.path
+ if write_protect:
+ fwrite(path, "1")
+ else:
+ fwrite(path, "0")
+
+ def _get_write_protect(self):
+ self._check_self()
+ path = "%s/write_protect" % self.path
+ write_protect = fread(path).strip()
+ if write_protect == "1":
+ return True
+ else:
+ return False
+
+ def _get_tpg_lun(self):
+ self._check_self()
+ path = os.path.realpath("%s/%s" % (self.path, self._get_alias()))
+ for lun in self.parent_nodeacl.parent_tpg.luns:
+ if lun.path == path:
+ return lun
+
+ raise RTSLibBrokenLink("Broken MappedLUN, no TPG LUN found !")
+
+ def _get_node_wwn(self):
+ self._check_self()
+ return self.parent_nodeacl.node_wwn
+
+ # MappedLUN public stuff
+
+ def delete(self):
+ '''
+ Delete the MappedLUN.
+ '''
+ self._check_self()
+ try:
+ lun_link = "%s/%s" % (self.path, self._get_alias())
+ except RTSLibBrokenLink:
+ pass
+ else:
+ if os.path.islink(lun_link):
+ os.unlink(lun_link)
+ super(MappedLUN, self).delete()
+
+ mapped_lun = property(_get_mapped_lun,
+ doc="Get the integer MappedLUN mapped_lun index.")
+ parent_nodeacl = property(_get_parent_nodeacl,
+ doc="Get the parent NodeACL object.")
+ write_protect = property(_get_write_protect, _set_write_protect,
+ doc="Get or set the boolean write protection.")
+ tpg_lun = property(_get_tpg_lun,
+ doc="Get the TPG LUN object the MappedLUN is pointing at.")
+ node_wwn = property(_get_node_wwn,
+ doc="Get the wwn of the node for which the TPG LUN is mapped.")
+
+class NodeACL(CFSNode):
+ '''
+ This is an interface to node ACLs in configFS.
+ A NodeACL is identified by the initiator node wwn and parent TPG.
+ '''
+
+ # NodeACL private stuff
+
+ def __init__(self, parent_tpg, node_wwn, mode='any'):
+ '''
+ @param parent_tpg: The parent TPG object.
+ @type parent_tpg: TPG
+ @param node_wwn: The wwn of the initiator node for which the ACL is
+ created.
+ @type node_wwn: string
+ @param mode:An optionnal string containing the object creation mode:
+ - I{'any'} means the configFS object will be either looked up or
+ created.
+ - I{'lookup'} means the object MUST already exist configFS.
+ - I{'create'} means the object must NOT already exist in configFS.
+ @type mode:string
+ @return: A NodeACL object.
+ '''
+
+ super(NodeACL, self).__init__()
+
+ if isinstance(parent_tpg, TPG):
+ self._parent_tpg = parent_tpg
+ else:
+ raise RTSLibError("Invalid parent TPG.")
+
+ self._node_wwn = str(node_wwn).lower()
+ self._path = "%s/acls/%s" % (self.parent_tpg.path, self.node_wwn)
+ self._create_in_cfs_ine(mode)
+
+ def _get_node_wwn(self):
+ return self._node_wwn
+
+ def _get_parent_tpg(self):
+ return self._parent_tpg
+
+ def _get_chap_mutual_password(self):
+ self._check_self()
+ path = "%s/auth/password_mutual" % self.path
+ value = fread(path).strip()
+ if value == "NULL":
+ return ''
+ else:
+ return value
+
+ def _set_chap_mutual_password(self, password):
+ self._check_self()
+ path = "%s/auth/password_mutual" % self.path
+ if password.strip() == '':
+ password = "NULL"
+ fwrite(path, "%s" % password)
+
+ def _get_chap_mutual_userid(self):
+ self._check_self()
+ path = "%s/auth/userid_mutual" % self.path
+ value = fread(path).strip()
+ if value == "NULL":
+ return ''
+ else:
+ return value
+
+ def _set_chap_mutual_userid(self, userid):
+ self._check_self()
+ path = "%s/auth/userid_mutual" % self.path
+ if userid.strip() == '':
+ userid = "NULL"
+ fwrite(path, "%s" % userid)
+
+ def _get_chap_password(self):
+ self._check_self()
+ path = "%s/auth/password" % self.path
+ value = fread(path).strip()
+ if value == "NULL":
+ return ''
+ else:
+ return value
+
+ def _set_chap_password(self, password):
+ self._check_self()
+ path = "%s/auth/password" % self.path
+ if password.strip() == '':
+ password = "NULL"
+ fwrite(path, "%s" % password)
+
+ def _get_chap_userid(self):
+ self._check_self()
+ path = "%s/auth/userid" % self.path
+ value = fread(path).strip()
+ if value == "NULL":
+ return ''
+ else:
+ return value
+
+ def _set_chap_userid(self, userid):
+ self._check_self()
+ path = "%s/auth/userid" % self.path
+ if userid.strip() == '':
+ userid = "NULL"
+ fwrite(path, "%s" % userid)
+
+ def _get_tcq_depth(self):
+ self._check_self()
+ path = "%s/cmdsn_depth" % self.path
+ return fread(path).strip()
+
+ def _set_tcq_depth(self, depth):
+ self._check_self()
+ path = "%s/cmdsn_depth" % self.path
+ fwrite(path, "%s" % depth)
+
+ def _get_authenticate_target(self):
+ self._check_self()
+ path = "%s/auth/authenticate_target" % self.path
+ if fread(path).strip() == "1":
+ return True
+ else:
+ return False
+
+ def _list_mapped_luns(self):
+ self._check_self()
+ mapped_luns = []
+ mapped_lun_dirs = glob.glob("%s/lun_*" % self.path)
+ for mapped_lun_dir in mapped_lun_dirs:
+ mapped_lun = int(os.path.basename(mapped_lun_dir).split("_")[1])
+ mapped_luns.append(MappedLUN(self, mapped_lun))
+ return mapped_luns
+
+ # NodeACL public stuff
+
+ def delete(self):
+ '''
+ Delete the NodeACL, including all MappedLUN objects.
+ If the underlying configFS object does not exist, this method does
+ nothing.
+ '''
+ self._check_self()
+ for mapped_lun in self.mapped_luns:
+ mapped_lun.delete()
+ super(NodeACL, self).delete()
+
+ def mapped_lun(self, mapped_lun, tpg_lun=None, write_protect=None):
+ '''
+ Same as MappedLUN() but without the parent_nodeacl parameter.
+ '''
+ self._check_self()
+ return MappedLUN(self, mapped_lun=mapped_lun, tpg_lun=tpg_lun,
+ write_protect=write_protect)
+
+ chap_userid = property(_get_chap_userid, _set_chap_userid,
+ doc="Set or get the initiator CHAP auth userid.")
+ chap_password = property(_get_chap_password, _set_chap_password,
+ doc=\
+ "Set or get the initiator CHAP auth password.")
+ chap_mutual_userid = property(_get_chap_mutual_userid,
+ _set_chap_mutual_userid,
+ doc=\
+ "Set or get the mutual CHAP auth userid.")
+ chap_mutual_password = property(_get_chap_mutual_password,
+ _set_chap_mutual_password,
+ doc=\
+ "Set or get the mutual CHAP password.")
+ tcq_depth = property(_get_tcq_depth, _set_tcq_depth,
+ doc="Set or get the TCQ depth for the initiator " \
+ + "sessions matching this NodeACL.")
+ parent_tpg = property(_get_parent_tpg,
+ doc="Get the parent TPG object.")
+ node_wwn = property(_get_node_wwn,
+ doc="Get the node wwn.")
+ authenticate_target = property(_get_authenticate_target,
+ doc="Get the boolean authenticate target flag.")
+ mapped_luns = property(_list_mapped_luns,
+ doc="Get the list of all MappedLUN objects in this NodeACL.")
+
+class NetworkPortal(CFSNode):
+ '''
+ This is an interface to NetworkPortals in configFS. A NetworkPortal is
+ identified by its IP and port, but here we also require the parent TPG, so
+ instance objects represent both the NetworkPortal and its association to a
+ TPG. This is necessary to get path information in order to create the
+ portal in the proper configFS hierarchy.
+ '''
+
+ # NetworkPortal private stuff
+
+ def __init__(self, parent_tpg, ip_address, port, mode='any'):
+ '''
+ @param parent_tpg: The parent TPG object.
+ @type parent_tpg: TPG
+ @param ip_address: The ipv4 IP address of the NetworkPortal.
+ @type ip_address: string
+ @param port: The NetworkPortal TCP/IP port.
+ @type port: int
+ @param mode:An optionnal string containing the object creation mode:
+ - I{'any'} means the configFS object will be either looked up or
+ created.
+ - I{'lookup'} means the object MUST already exist configFS.
+ - I{'create'} means the object must NOT already exist in configFS.
+ @type mode:string
+ @return: A NetworkPortal object.
+ '''
+
+ super(NetworkPortal, self).__init__()
+ if not re.match("^(25[0-5]|2[0-4]\d|[01]\d{2}|\d{1,2})"
+ + "(\.(25[0-5]|2[0-4]\d|[01]\d{2}|\d{1,2})){3}$",
+ str(ip_address)):
+ raise RTSLibError("Invalid IP address.")
+ else:
+ self._ip_address = str(ip_address)
+
+ try:
+ self._port = int(port)
+ except ValueError:
+ raise RTSLibError("Invalid port.")
+
+ if isinstance(parent_tpg, TPG):
+ self._parent_tpg = parent_tpg
+ else:
+ raise RTSLibError("Invalid parent TPG.")
+
+ self._path = "%s/np/%s:%d" \
+ % (self.parent_tpg.path, self.ip_address, self.port)
+ self._create_in_cfs_ine(mode)
+
+ def _get_ip_address(self):
+ return self._ip_address
+
+ def _get_port(self):
+ return self._port
+
+ def _get_parent_tpg(self):
+ return self._parent_tpg
+
+ # NetworkPortal public stuff
+
+ parent_tpg = property(_get_parent_tpg,
+ doc="Get the parent TPG object.")
+ port = property(_get_port,
+ doc="Get the NetworkPortal's TCP port as an int.")
+ ip_address = property(_get_ip_address,
+ doc="Get the NetworkPortal's IP address as a string.")
+
+class TPG(CFSNode):
+ '''
+ This is a an interface to Target Portal Groups in configFS.
+ A TPG is identified by its parent Target object and its TPG Tag.
+ To a TPG object is attached a list of NetworkPortals. Targets without
+ the 'tpgts' feature cannot have more than a single TPG, so attempts
+ to create more will raise an exception.
+ '''
+
+ # TPG private stuff
+
+ def __init__(self, parent_target, tag, mode='any'):
+ '''
+ @param parent_target: The parent Target object of the TPG.
+ @type parent_target: Target
+ @param tag: The TPG Tag (TPGT).
+ @type tag: int > 0
+ @param mode:An optionnal string containing the object creation mode:
+ - I{'any'} means the configFS object will be either looked up or
+ created.
+ - I{'lookup'} means the object MUST already exist configFS.
+ - I{'create'} means the object must NOT already exist in configFS.
+ @type mode:string
+ @return: A TPG object.
+ '''
+
+ super(TPG, self).__init__()
+
+ try:
+ self._tag = int(tag)
+ except ValueError:
+ raise RTSLibError("Invalid Tag.")
+
+ if tag < 1:
+ raise RTSLibError("Invalig Tag, it must be >0.")
+
+ if isinstance(parent_target, Target):
+ self._parent_target = parent_target
+ else:
+ raise RTSLibError("Invalid parent Target.")
+
+ self._path = "%s/tpgt_%d" % (self.parent_target.path, self.tag)
+
+ target_path = self.parent_target.path
+ if not self.has_feature('tpgts') and not os.path.isdir(self._path):
+ for filename in os.listdir(target_path):
+ if filename.startswith("tpgt_") \
+ and os.path.isdir("%s/%s" % (target_path, filename)) \
+ and filename != "tpgt_%d" % self.tag:
+ raise RTSLibError("Target cannot have multiple TPGs.")
+
+ self._create_in_cfs_ine(mode)
+ if self.has_feature('nexus') and not self._get_nexus():
+ self._set_nexus()
+
+ def _get_tag(self):
+ return self._tag
+
+ def _get_parent_target(self):
+ return self._parent_target
+
+ def _list_network_portals(self):
+ self._check_self()
+ if not self.has_feature('nps'):
+ return []
+ network_portals = []
+ network_portal_dirs = os.listdir("%s/np" % self.path)
+ for network_portal_dir in network_portal_dirs:
+ (ip_address, port) = \
+ os.path.basename(network_portal_dir).split(":")
+ port = int(port)
+ network_portals.append(
+ NetworkPortal(self, ip_address, port, 'lookup'))
+ return network_portals
+
+ def _get_enable(self):
+ self._check_self()
+ path = "%s/enable" % self.path
+ # If the TPG does not have the enable attribute, then it is always
+ # enabled.
+ if os.path.isfile(path):
+ return int(fread(path))
+ else:
+ return 1
+
+ def _set_enable(self, boolean):
+ '''
+ Enables or disables the TPG. Raises an error if trying to disable a TPG
+ without en enable attribute (but enabling works in that case).
+ '''
+ self._check_self()
+ path = "%s/enable" % self.path
+ if os.path.isfile(path):
+ if boolean and not self._get_enable():
+ fwrite(path, "1")
+ elif not boolean and self._get_enable():
+ fwrite(path, "0")
+ elif not boolean:
+ raise RTSLibError("TPG cannot be disabled.")
+
+ def _get_nexus(self):
+ '''
+ Gets the nexus initiator WWN, or None if the TPG does not have one.
+ '''
+ self._check_self()
+ if self.has_feature('nexus'):
+ try:
+ nexus_wwn = fread("%s/nexus" % self.path).strip()
+ except IOError:
+ nexus_wwn = ''
+ return nexus_wwn
+ else:
+ return None
+
+ def _set_nexus(self, nexus_wwn=None):
+ '''
+ Sets the nexus initiator WWN. Raises an exception if the nexus is
+ already set or if the TPG does not use a nexus.
+ '''
+ self._check_self()
+ if not self.has_feature('nexus'):
+ raise RTSLibError("The TPG does not use a nexus.")
+ elif self._get_nexus():
+ raise RTSLibError("The TPG's nexus initiator WWN is already set.")
+ else:
+ if nexus_wwn is None:
+ nexus_wwn = generate_wwn(self.parent_target.wwn_type)
+ elif not is_valid_wwn(self.parent_target.wwn_type, nexus_wwn):
+ raise RTSLibError("WWN '%s' is not of type '%s'."
+ % (nexus_wwn, self.parent_target.wwn_type))
+ fwrite("%s/nexus" % self.path, nexus_wwn)
+
+ def _create_in_cfs_ine(self, mode):
+ super(TPG, self)._create_in_cfs_ine(mode)
+ if not os.path.isdir(self.alua_metadata_path):
+ os.makedirs(self.alua_metadata_path)
+
+ def _list_node_acls(self):
+ self._check_self()
+ if not self.has_feature('acls'):
+ return []
+ node_acls = []
+ node_acl_dirs = [os.path.basename(path)
+ for path in os.listdir("%s/acls" % self.path)]
+ for node_acl_dir in node_acl_dirs:
+ node_acls.append(NodeACL(self, node_acl_dir, 'lookup'))
+ return node_acls
+
+ def _list_luns(self):
+ self._check_self()
+ luns = []
+ lun_dirs = [os.path.basename(path)
+ for path in os.listdir("%s/lun" % self.path)]
+ for lun_dir in lun_dirs:
+ lun = lun_dir.split('_')[1]
+ lun = int(lun)
+ luns.append(LUN(self, lun))
+ return luns
+
+ def _control(self, command):
+ self._check_self()
+ path = "%s/control" % self.path
+ fwrite(path, "%s\n" % str(command))
+
+ def _get_alua_metadata_path(self):
+ return "%s/%s+%d" \
+ % (self.alua_metadata_dir, self.parent_target.wwn, self.tag)
+
+ # TPG public stuff
+
+ def has_feature(self, feature):
+ '''
+ Whether or not this TPG has a certain feature.
+ '''
+ return self.parent_target.has_feature(feature)
+
+ def delete(self):
+ '''
+ Recursively deletes a TPG object.
+ This will delete all attached LUN, NetworkPortal and Node ACL objects
+ and then the TPG itself. Before starting the actual deletion process,
+ all sessions will be disconnected.
+ '''
+ self._check_self()
+
+ path = "%s/enable" % self.path
+ if os.path.isfile(path):
+ self.enable = False
+
+ for acl in self.node_acls:
+ acl.delete()
+ for lun in self.luns:
+ lun.delete()
+ for portal in self.network_portals:
+ portal.delete()
+ super(TPG, self).delete()
+ # TODO: check that ALUA MD removal works while removing TPG
+ if os.path.isdir(self.alua_metadata_path):
+ shutil.rmtree(self.alua_metadata_path)
+
+ def node_acl(self, node_wwn, mode='any'):
+ '''
+ Same as NodeACL() but without specifying the parent_tpg.
+ '''
+ self._check_self()
+ return NodeACL(self, node_wwn=node_wwn, mode=mode)
+
+ def network_portal(self, ip_address, port, mode='any'):
+ '''
+ Same as NetworkPortal() but without specifying the parent_tpg.
+ '''
+ self._check_self()
+ return NetworkPortal(self, ip_address=ip_address, port=port, mode=mode)
+
+ def lun(self, lun, storage_object=None, alias=None):
+ '''
+ Same as LUN() but without specifying the parent_tpg.
+ '''
+ self._check_self()
+ return LUN(self, lun=lun, storage_object=storage_object, alias=alias)
+
+ alua_metadata_path = property(_get_alua_metadata_path,
+ doc="Get the ALUA metadata directory path " \
+ + "for the TPG.")
+ tag = property(_get_tag,
+ doc="Get the TPG Tag as an int.")
+ parent_target = property(_get_parent_target,
+ doc="Get the parent Target object to which the " \
+ + "TPG is attached.")
+ enable = property(_get_enable, _set_enable,
+ doc="Get or set a boolean value representing the " \
+ + "enable status of the TPG. " \
+ + "True means the TPG is enabled, False means it is " \
+ + "disabled.")
+ network_portals = property(_list_network_portals,
+ doc="Get the list of NetworkPortal objects currently attached " \
+ + "to the TPG.")
+ node_acls = property(_list_node_acls,
+ doc="Get the list of NodeACL objects currently " \
+ + "attached to the TPG.")
+ luns = property(_list_luns,
+ doc="Get the list of LUN objects currently attached " \
+ + "to the TPG.")
+
+ nexus = property(_get_nexus, _set_nexus,
+ doc="Get or set (once) the TPG's Nexus is used.")
+
+class Target(CFSNode):
+ '''
+ This is an interface to Targets in configFS.
+ A Target is identified by its wwn.
+ To a Target is attached a list of TPG objects.
+ '''
+
+ # Target private stuff
+
+ def __init__(self, fabric_module, wwn=None, mode='any'):
+ '''
+ @param fabric_module: The target's fabric module.
+ @type fabric_module: FabricModule
+ @param wwn: The optionnal Target's wwn.
+ If no wwn or an empty wwn is specified, one will be generated
+ for you.
+ @type wwn: string
+ @param mode:An optionnal string containing the object creation mode:
+ - I{'any'} means the configFS object will be either looked up
+ or created.
+ - I{'lookup'} means the object MUST already exist configFS.
+ - I{'create'} means the object must NOT already exist in configFS.
+ @type mode:string
+ @return: A Target object.
+ '''
+
+ super(Target, self).__init__()
+ self.fabric_module = fabric_module
+ self.wwn_type = fabric_module.spec['wwn_type']
+
+ if wwn is not None:
+ wwn = str(wwn).strip()
+ elif fabric_module.spec['wwn_list']:
+ existing_wwns = set([child.wwn for child in fabric_module.targets])
+ free_wwns = fabric_module.spec['wwn_list'] - existing_wwns
+ if free_wwns:
+ wwn = free_wwns.pop()
+ else:
+ raise RTSLibError("All WWN are in use, can't create target.")
+ else:
+ wwn = generate_wwn(self.wwn_type)
+
+ self.wwn = wwn
+ self._path = "%s/%s" % (self.fabric_module.path, self.wwn)
+ if not self:
+ if not self.fabric_module.is_valid_wwn(self.wwn):
+ raise RTSLibError("Invalid %s wwn: %s"
+ % (self.wwn_type, self.wwn))
+ self._create_in_cfs_ine(mode)
+
+ def _list_tpgs(self):
+ self._check_self()
+ tpgs = []
+ tpg_dirs = glob.glob("%s/tpgt*" % self.path)
+ for tpg_dir in tpg_dirs:
+ tag = os.path.basename(tpg_dir).split('_')[1]
+ tag = int(tag)
+ tpgs.append(TPG(self, tag, 'lookup'))
+ return tpgs
+
+ # Target public stuff
+
+ def has_feature(self, feature):
+ '''
+ Whether or not this Target has a certain feature.
+ '''
+ return self.fabric_module.has_feature(feature)
+
+ def delete(self):
+ '''
+ Recursively deletes a Target object.
+ This will delete all attached TPG objects and then the Target itself.
+ '''
+ self._check_self()
+ for tpg in self.tpgs:
+ tpg.delete()
+ super(Target, self).delete()
+
+ tpgs = property(_list_tpgs, doc="Get the list of TPG for the Target.")
+
+def _test():
+ import doctest
+ doctest.testmod()
+
+if __name__ == "__main__":
+ _test()
diff --git a/rtslib/tcm.py b/rtslib/tcm.py
new file mode 100644
index 0000000..6d47604
--- /dev/null
+++ b/rtslib/tcm.py
@@ -0,0 +1,1130 @@
+'''
+Implements the RTS Target backstore and storage object classes.
+
+This file is part of RTSLib Community Edition.
+Copyright (c) 2011 by RisingTide Systems LLC
+
+This program is free software: you can redistribute it and/or modify
+it under the terms of the GNU Affero General Public License as
+published by the Free Software Foundation, version 3 (AGPLv3).
+
+This program is distributed in the hope that it will be useful,
+but WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+GNU Affero General Public License for more details.
+
+You should have received a copy of the GNU Affero General Public License
+along with this program. If not, see <http://www.gnu.org/licenses/>.
+'''
+
+import os
+import re
+
+from target import LUN, TPG, Target, FabricModule
+from node import CFSNode
+from utils import fread, fwrite, RTSLibError, list_scsi_hbas, generate_wwn
+from utils import convert_scsi_path_to_hctl, convert_scsi_hctl_to_path
+from utils import convert_human_to_bytes, is_dev_in_use, get_block_type
+from utils import is_disk_partition, get_disk_size
+
+class Backstore(CFSNode):
+
+ # Backstore private stuff
+
+ def __init__(self, plugin, storage_class, index, mode):
+ super(Backstore, self).__init__()
+ if issubclass(storage_class, StorageObject):
+ self._storage_object_class = storage_class
+ self._plugin = plugin
+ else:
+ raise RTSLibError("StorageClass must derive from StorageObject.")
+ try:
+ self._index = int(index)
+ except ValueError:
+ raise RTSLibError("Invalid backstore index: %s" % index)
+ self._path = "%s/core/%s_%d" % (self.configfs_dir,
+ self._plugin,
+ self._index)
+ self._create_in_cfs_ine(mode)
+
+ def _get_plugin(self):
+ return self._plugin
+
+ def _get_index(self):
+ return self._index
+
+ def _list_storage_objects(self):
+ self._check_self()
+ storage_objects = []
+ storage_object_names = [os.path.basename(s)
+ for s in os.listdir(self.path)
+ if s not in set(["hba_info", "hba_mode"])]
+
+ for storage_object_name in storage_object_names:
+ storage_objects.append(self._storage_object_class(
+ self, storage_object_name))
+
+ return storage_objects
+
+ def _create_in_cfs_ine(self, mode):
+ try:
+ super(Backstore, self)._create_in_cfs_ine(mode)
+ except OSError, msg:
+ raise RTSLibError("Cannot create backstore: %s" % msg)
+
+ def _parse_info(self, key):
+ self._check_self()
+ info = fread("%s/hba_info" % self.path)
+ return re.search(".*%s: ([^: ]+).*" \
+ % key, ' '.join(info.split())).group(1).lower()
+
+ def _get_version(self):
+ self._check_self()
+ return self._parse_info("version")
+
+ def _get_plugin(self):
+ self._check_self()
+ return self._parse_info("plugin")
+
+ def _get_name(self):
+ self._check_self()
+ return "%s%d" % (self.plugin, self.index)
+
+
+ # Backstore public stuff
+
+ def delete(self):
+ '''
+ Recursively deletes a Backstore object.
+ This will delete all attached StorageObject objects, and then the
+ Backstore itself. The underlying file and block storages will not be
+ touched, but all ramdisk data will be lost.
+ '''
+ self._check_self()
+ for storage in self.storage_objects:
+ storage.delete()
+ super(Backstore, self).delete()
+
+ plugin = property(_get_plugin,
+ doc="Get the backstore plugin name.")
+ index = property(_get_index,
+ doc="Get the backstore index as an int.")
+ storage_objects = property(_list_storage_objects,
+ doc="Get the list of StorageObjects attached to the backstore.")
+ version = property(_get_version,
+ doc="Get the Backstore plugin version string.")
+ plugin = property(_get_plugin,
+ doc="Get the Backstore plugin name.")
+ name = property(_get_name,
+ doc="Get the backstore name.")
+
+class PSCSIBackstore(Backstore):
+ '''
+ This is an interface to pscsi backstore plugin objects in configFS.
+ A PSCSIBackstore object is identified by its backstore index.
+ '''
+
+ # PSCSIBackstore private stuff
+
+ def __init__(self, index, mode='any', legacy=False):
+ '''
+ @param index: The backstore index matching a physical SCSI HBA.
+ @type index: int
+ @param mode: An optionnal string containing the object creation mode:
+ - I{'any'} the configFS object will be either lookuped or created.
+ - I{'lookup'} the object MUST already exist configFS.
+ - I{'create'} the object must NOT already exist in configFS.
+ @type mode:string
+ @param legacy: Enable legacy physcal HBA mode. If True, you must
+ specify it also in lookup mode for StorageObjects to be notified.
+ You've been warned !
+ @return: A PSCSIBackstore object.
+ '''
+ self._legacy = legacy
+ super(PSCSIBackstore, self).__init__("pscsi",
+ PSCSIStorageObject,
+ index,
+ mode)
+
+ def _create_in_cfs_ine(self, mode):
+ if self.legacy_mode and self._index not in list_scsi_hbas():
+ raise RTSLibError("Cannot create backstore, hba "
+ + "scsi%d does not exist."
+ % self._index)
+ else:
+ Backstore._create_in_cfs_ine(self, mode)
+
+ def _get_legacy(self):
+ return self._legacy
+
+ # PSCSIBackstore public stuff
+
+ def storage_object(self, name, dev=None):
+ '''
+ Same as PSCSIStorageObject() without specifying the backstore
+ '''
+ self._check_self()
+ return PSCSIStorageObject(self, name=name, dev=dev)
+
+ legacy_mode = property(_get_legacy,
+ doc="Get the legacy mode flag. If True, the Vitualbackstore "
+ + " index must match the StorageObjects real HBAs.")
+
+class RDDRBackstore(Backstore):
+ '''
+ This is an interface to rd_dr backstore plugin objects in configFS.
+ A RDDRBackstore object is identified by its backstore index.
+ '''
+
+ # RDDRBackstore private stuff
+
+ def __init__(self, index, mode='any'):
+ '''
+ @param index: The backstore index.
+ @type index: int
+ @param mode: An optionnal string containing the object creation mode:
+ - I{'any'} the configFS object will be either lookupd or created.
+ - I{'lookup'} the object MUST already exist configFS.
+ - I{'create'} the object must NOT already exist in configFS.
+ @type mode:string
+ @return: A RDDRBackstore object.
+ '''
+
+ super(RDDRBackstore, self).__init__("rd_dr", RDDRStorageObject,
+ index, mode)
+
+ # RDDRBackstore public stuff
+
+ def storage_object(self, name, size=None, gen_wwn=True):
+ '''
+ Same as RDDRStorageObject() without specifying the backstore
+ '''
+ self._check_self()
+ return RDDRStorageObject(self, name=name,
+ size=size, gen_wwn=gen_wwn)
+
+class RDMCPBackstore(Backstore):
+ '''
+ This is an interface to rd_mcp backstore plugin objects in configFS.
+ A RDMCPBackstore object is identified by its backstore index.
+ '''
+
+ # RDMCPBackstore private stuff
+
+ def __init__(self, index, mode='any'):
+ '''
+ @param index: The backstore index.
+ @type index: int
+ @param mode: An optionnal string containing the object creation mode:
+ - I{'any'} the configFS object will be either lookupd or created.
+ - I{'lookup'} the object MUST already exist configFS.
+ - I{'create'} the object must NOT already exist in configFS.
+ @type mode:string
+ @return: A RDMCPBackstore object.
+ '''
+
+ super(RDMCPBackstore, self).__init__("rd_mcp", RDMCPStorageObject,
+ index, mode)
+
+ # RDMCPBackstore public stuff
+
+ def storage_object(self, name, size=None, gen_wwn=True):
+ '''
+ Same as RDMCPStorageObject() without specifying the backstore
+ '''
+ self._check_self()
+ return RDMCPStorageObject(self, name=name,
+ size=size, gen_wwn=gen_wwn)
+
+class FileIOBackstore(Backstore):
+ '''
+ This is an interface to fileio backstore plugin objects in configFS.
+ A FileIOBackstore object is identified by its backstore index.
+ '''
+
+ # FileIOBackstore private stuff
+
+ def __init__(self, index, mode='any'):
+ '''
+ @param index: The backstore index.
+ @type index: int
+ @param mode: An optionnal string containing the object creation mode:
+ - I{'any'} the configFS object will be either lookuped or created.
+ - I{'lookup'} the object MUST already exist configFS.
+ - I{'create'} the object must NOT already exist in configFS.
+ @type mode:string
+ @return: A FileIOBackstore object.
+ '''
+
+ super(FileIOBackstore, self).__init__("fileio", FileIOStorageObject,
+ index, mode)
+
+ # FileIOBackstore public stuff
+
+ def storage_object(self, name, dev=None, size=None,
+ gen_wwn=True, buffered_mode=False):
+ '''
+ Same as FileIOStorageObject() without specifying the backstore
+ '''
+ self._check_self()
+ return FileIOStorageObject(self, name=name, dev=dev,
+ size=size, gen_wwn=gen_wwn,
+ buffered_mode=buffered_mode)
+
+class IBlockBackstore(Backstore):
+ '''
+ This is an interface to iblock backstore plugin objects in configFS.
+ An IBlockBackstore object is identified by its backstore index.
+ '''
+
+ # IBlockBackstore private stuff
+
+ def __init__(self, index, mode='any'):
+ '''
+ @param index: The backstore index.
+ @type index: int
+ @param mode: An optionnal string containing the object creation mode:
+ - I{'any'} the configFS object will be either lookupd or created.
+ - I{'lookup'} the object MUST already exist configFS.
+ - I{'create'} the object must NOT already exist in configFS.
+ @type mode:string
+ @return: An IBlockBackstore object.
+ '''
+
+ super(IBlockBackstore, self).__init__("iblock", IBlockStorageObject,
+ index, mode)
+
+ # IBlockBackstore public stuff
+
+ def storage_object(self, name, dev=None, gen_wwn=True):
+ '''
+ Same as IBlockStorageObject() without specifying the backstore
+ '''
+ self._check_self()
+ return IBlockStorageObject(self, name=name, dev=dev,
+ gen_wwn=gen_wwn)
+
+class StorageObject(CFSNode):
+ '''
+ This is an interface to storage objects in configFS. A StorageObject is
+ identified by its backstore and its name.
+ '''
+ # StorageObject private stuff
+
+ def __init__(self, backstore, backstore_class, name, mode):
+ if not isinstance(backstore, backstore_class):
+ raise RTSLibError("The parent backstore must be of "
+ + "type %s" % backstore_class.__name__)
+ super(StorageObject, self).__init__()
+ self._backstore = backstore
+ if "/" in name or " " in name or "\t" in name or "\n" in name:
+ raise RTSLibError("A storage object's name cannot contain "
+ " /, newline or spaces/tabs.")
+ else:
+ self._name = name
+ self._path = "%s/%s" % (self.backstore.path, self.name)
+ self._create_in_cfs_ine(mode)
+
+ def _get_wwn(self):
+ self._check_self()
+ if self.is_configured():
+ path = "%s/wwn/vpd_unit_serial" % self.path
+ return fread(path).partition(":")[2].strip()
+ else:
+ return ""
+
+ def _set_wwn(self, wwn):
+ self._check_self()
+ if self.is_configured():
+ path = "%s/wwn/vpd_unit_serial" % self.path
+ fwrite(path, "%s\n" % wwn)
+ else:
+ raise RTSLibError("Cannot write a T10 WWN Unit Serial to "
+ + "an unconfigured StorageObject.")
+
+ def _set_udev_path(self, udev_path):
+ self._check_self()
+ path = "%s/udev_path" % self.path
+ fwrite(path, "%s" % udev_path)
+
+ def _get_udev_path(self):
+ self._check_self()
+ path = "%s/udev_path" % self.path
+ udev_path = fread(path).strip()
+ if not udev_path and self.backstore.plugin == "fileio":
+ udev_path = self._parse_info('File').strip()
+ return udev_path
+
+ def _get_name(self):
+ return self._name
+
+ def _get_backstore(self):
+ return self._backstore
+
+ def _enable(self):
+ self._check_self()
+ path = "%s/enable" % self.path
+ fwrite(path, "1\n")
+
+ def _control(self, command):
+ self._check_self()
+ path = "%s/control" % self.path
+ fwrite(path, "%s" % str(command).strip())
+
+ def _write_fd(self, contents):
+ self._check_self()
+ path = "%s/fd" % self.path
+ fwrite(path, "%s" % str(contents).strip())
+
+ def _parse_info(self, key):
+ self._check_self()
+ info = fread("%s/info" % self.path)
+ return re.search(".*%s: ([^: ]+).*" \
+ % key, ' '.join(info.split())).group(1).lower()
+
+ def _get_status(self):
+ self._check_self()
+ return self._parse_info('Status')
+
+ def _gen_attached_luns(self):
+ '''
+ This function is used only by delete(). The idea is that delete() can
+ check when there are no more attached LUNs because when the last one is
+ deleted, the status of the StorageObject changes. Then, we can stop
+ fetching the values. The list comprehension of _list_attached_luns
+ cannot be used here because of limitations in python 2.5.
+ '''
+ islink = os.path.islink
+ listdir = os.listdir
+ realpath = os.path.realpath
+ path = self.path
+ from root import RTSRoot
+ rtsroot = RTSRoot()
+ target_names_excludes = FabricModule.target_names_excludes
+
+ for fabric_module in rtsroot.loaded_fabric_modules:
+ base = fabric_module.path
+ for tgt_dir in listdir(base):
+ if tgt_dir not in target_names_excludes:
+ tpgt_dirs = "%s/%s" % (base, tgt_dir)
+ for tpgt_dir in listdir(tpgt_dirs):
+ lun_dirs = "%s/%s/%s/lun" % (base, tgt_dir, tpgt_dir)
+ for lun_dir in listdir(lun_dirs):
+ lun_files = "%s/%s/%s" % (base, tgt_dir, tpgt_dir)
+ lun_files += "/lun/%s" % lun_dir
+ for lun_file in listdir(lun_files):
+ link = "%s/%s/%s/" % (base, tgt_dir, tpgt_dir)
+ link += "/lun/%s/%s" % (lun_dir, lun_file)
+ if islink(link) and realpath(link) == path:
+ val = (tpgt_dir + "_" + lun_dir).split('_')
+ target = Target(fabric_module, tgt_dir)
+ yield LUN(TPG(target, val[1]), val[3])
+
+ def _list_attached_luns(self):
+ '''
+ Code below is ugly and hairy, but this is the result of a lot of tests
+ for optimizing the speed of it. islink() is slower, glob is slower,
+ direct object parsing is slower, etc. Do not touch without retesting
+ mass deletion of storage objects with lots of luns attached ! Besides,
+ generators on the whole object chain whould actually SLOW things down
+ becauser we need to parse the whole list, not stop at first or n-th
+ match. Also tried joins, intermediate path save & reuse, etc. It is
+ approx. 20 times faster than using root.luns and matching path on them.
+ '''
+ # TODO: Handle SAS loopback LUN
+ self._check_self()
+
+ # The StorageObject is not in use, so there are no luns attached.
+ if self.status == 'deactivated':
+ return []
+ luns = set([])
+ listdir = os.listdir
+ realpath = os.path.realpath
+ path = self.path
+
+ xwwn = FabricModule.target_names_excludes
+ xlink = ["alua_tg_pt_write_md", "alua_tg_pt_status",
+ "alua_tg_pt_offline", "alua_tg_pt_gp"]
+ from root import RTSRoot
+ rtsroot = RTSRoot()
+ for fabric_module in rtsroot.loaded_fabric_modules:
+ base = fabric_module.path
+ luns.update(set(
+ [LUN(TPG(Target(wwn), tpgt.split("_")[1]), lun.split("_")[1])
+ for wwn in listdir(base) if wwn not in xwwn
+ for tpgt in listdir(
+ "%s/%s" % (base,wwn))
+ for lun in listdir(
+ "%s/%s/%s/lun" % (base, wwn, tpgt))
+ for link in listdir(
+ "%s/%s/%s/lun/%s" % (base, wwn, tpgt, lun))
+ if link not in xlink
+ if realpath("%s/%s/%s/lun/%s/%s"
+ % (base, wwn, tpgt, lun, link)) == path]))
+ return luns
+
+ # StorageObject public stuff
+
+ def delete(self):
+ '''
+ Recursively deletes a StorageObject object.
+ This will delete all attached LUNs currently using the StorageObject
+ object, and then the StorageObject itself. The underlying file and
+ block storages will not be touched, but all ramdisk data will be lost.
+ '''
+ self._check_self()
+
+ # If we are called after a configure error, we can skip this
+ if self.is_configured():
+ gen = self._gen_attached_luns()
+ while self.status == 'activated':
+ gen.next().delete()
+
+ # FIXME: delete ALUA tp pt gps
+ # FIXME: delete snapshots
+
+ super(StorageObject, self).delete()
+
+ def is_configured(self):
+ '''
+ @return: True if the StorageObject is configured, else returns False
+ '''
+
+ self._check_self()
+ path = "%s/info" % self.path
+ try:
+ fread(path)
+ except IOError:
+ return False
+ else:
+ return True
+
+ backstore = property(_get_backstore,
+ doc="Get the backstore object.")
+ name = property(_get_name,
+ doc="Get the StorageObject name as a string.")
+ udev_path = property(_get_udev_path,
+ doc="Get the StorageObject udev_path as a string.")
+ wwn = property(_get_wwn, _set_wwn,
+ doc="Get or set the StorageObject T10 WWN Serial as a string.")
+ status = property(_get_status,
+ doc="Get the storage object status, depending on wether or not it"\
+ + "is used by any LUN")
+ attached_luns = property(_list_attached_luns,
+ doc="Get the list of all LUN objects attached.")
+
+class PSCSIStorageObject(StorageObject):
+ '''
+ An interface to configFS storage objects for pscsi backstore.
+ '''
+
+ # PSCSIStorageObject private stuff
+
+ def __init__(self, backstore, name, dev=None):
+ '''
+ A PSCSIStorageObject can be instanciated in two ways:
+ - B{Creation mode}: If I{dev} is specified, the underlying configFS
+ object will be created with that parameter. No PSCSIStorageObject
+ with the same I{name} can pre-exist in the parent PSCSIBackstore
+ in that mode, or instanciation will fail.
+ - B{Lookup mode}: If I{dev} is not set, then the PSCSIStorageObject
+ will be bound to the existing configFS object in the parent
+ PSCSIBackstore having the specified I{name}. The underlying
+ configFS object must already exist in that mode, or instanciation
+ will fail.
+
+ @param backstore: The parent backstore of the PSCSIStorageObject.
+ @type backstore: PSCSIBackstore
+ @param name: The name of the PSCSIStorageObject.
+ @type name: string
+ @param dev: You have two choices:
+ - Use the SCSI id of the device: I{dev="H:C:T:L"}. If the parent
+ backstore is in legacy mode, you must use I{dev="C:T:L"}
+ instead, as the backstore index of the SCSI dev device would then be
+ constrained by the parent backstore index.
+ - Use the path to the SCSI device: I{dev="/path/to/dev"}.
+ Note that if the parent Backstore is in legacy mode, the device
+ must have the same backstore index as the parent backstore.
+ @type dev: string
+ @return: A PSCSIStorageObject object.
+ '''
+ if dev is not None:
+ super(PSCSIStorageObject, self).__init__(backstore,
+ PSCSIBackstore,
+ name, 'create')
+ try:
+ self._configure(dev)
+ except:
+ self.delete()
+ raise
+ else:
+ super(PSCSIStorageObject, self).__init__(backstore,
+ PSCSIBackstore,
+ name, 'lookup')
+
+ def _configure(self, dev):
+ self._check_self()
+ parent_hostid = self.backstore.index
+ legacy = self.backstore.legacy_mode
+ if legacy:
+ try:
+ (hostid, channelid, targetid, lunid) = \
+ convert_scsi_path_to_hctl(dev)
+ except TypeError:
+ try:
+ (channelid, targetid, lunid) = dev.split(':')
+ channelid = int(channelid)
+ targetid = int(targetid)
+ lunid = int(lunid)
+ except ValueError:
+ raise RTSLibError("Cannot find SCSI device by "
+ + "path, and dev parameter not "
+ + "in C:T:L format: %s." % dev)
+ else:
+ udev_path = convert_scsi_hctl_to_path(parent_hostid,
+ channelid,
+ targetid,
+ lunid)
+ if not udev_path:
+ raise RTSLibError("SCSI device does not exist.")
+ else:
+ if hostid != parent_hostid:
+ raise RTSLibError("The specified SCSI device does "
+ + "not belong to the backstore.")
+ else:
+ udev_path = dev.strip()
+ else:
+ # The Backstore is not in legacy mode.
+ # Use H:C:T:L format or preserve the path given by the user.
+ try:
+ (hostid, channelid, targetid, lunid) = \
+ convert_scsi_path_to_hctl(dev)
+ except TypeError:
+ try:
+ (hostid, channelid, targetid, lunid) = dev.split(':')
+ hostid = int(hostid)
+ channelid = int(channelid)
+ targetid = int(targetid)
+ lunid = int(lunid)
+ except ValueError:
+ raise RTSLibError("Cannot find SCSI device by "
+ + "path, and dev "
+ + "parameter not in H:C:T:L "
+ + "format: %s." % dev)
+ else:
+ udev_path = convert_scsi_hctl_to_path(hostid,
+ channelid,
+ targetid,
+ lunid)
+ if not udev_path:
+ raise RTSLibError("SCSI device does not exist.")
+ else:
+ udev_path = dev.strip()
+
+ if is_dev_in_use(udev_path):
+ raise RTSLibError("Cannot configure StorageObject because "
+ + "device %s (SCSI %d:%d:%d:%d) "
+ % (udev_path, hostid, channelid,
+ targetid, lunid)
+ + "is already in use.")
+
+ if legacy:
+ self._control("scsi_channel_id=%d," % channelid \
+ + "scsi_target_id=%d," % targetid \
+ + "scsi_lun_id=%d" % lunid)
+ else:
+ self._control("scsi_host_id=%d," % hostid \
+ + "scsi_channel_id=%d," % channelid \
+ + "scsi_target_id=%d," % targetid \
+ + "scsi_lun_id=%d" % lunid)
+ self._set_udev_path(udev_path)
+ self._enable()
+
+ def _get_wwn(self):
+ self._check_self()
+ if self.is_configured():
+ path = "%s/wwn/vpd_unit_serial" % self.path
+ return fread(path).partition(":")[2].strip()
+ else:
+ return ""
+
+ def _get_model(self):
+ self._check_self()
+ info = fread("%s/info" % self.path)
+ return str(re.search(".*Model:(.*)Rev:",
+ ' '.join(info.split())).group(1)).strip()
+
+ def _get_vendor(self):
+ self._check_self()
+ info = fread("%s/info" % self.path)
+ return str(re.search(".*Vendor:(.*)Model:",
+ ' '.join(info.split())).group(1)).strip()
+
+ def _get_revision(self):
+ self._check_self()
+ return self._parse_info('Rev')
+
+ def _get_channel_id(self):
+ self._check_self()
+ return int(self._parse_info('Channel ID'))
+
+ def _get_target_id(self):
+ self._check_self()
+ return int(self._parse_info('Target ID'))
+
+ def _get_lun(self):
+ self._check_self()
+ return int(self._parse_info('LUN'))
+
+ def _get_host_id(self):
+ self._check_self()
+ return int(self._parse_info('Host ID'))
+
+ # PSCSIStorageObject public stuff
+
+ wwn = property(_get_wwn,
+ doc="Get the StorageObject T10 WWN Unit Serial as a string."
+ + " You cannot set it for pscsi-backed StorageObjects.")
+ model = property(_get_model,
+ doc="Get the SCSI device model string")
+ vendor = property(_get_vendor,
+ doc="Get the SCSI device vendor string")
+ revision = property(_get_revision,
+ doc="Get the SCSI device revision string")
+ host_id = property(_get_host_id,
+ doc="Get the SCSI device host id")
+ channel_id = property(_get_channel_id,
+ doc="Get the SCSI device channel id")
+ target_id = property(_get_target_id,
+ doc="Get the SCSI device target id")
+ lun = property(_get_lun,
+ doc="Get the SCSI device LUN")
+
+class RDDRStorageObject(StorageObject):
+ '''
+ An interface to configFS storage objects for rd_dr backstore.
+ '''
+
+ # RDDRStorageObject private stuff
+
+ def __init__(self, backstore, name, size=None, gen_wwn=True):
+ '''
+ A RDDRStorageObject can be instanciated in two ways:
+ - B{Creation mode}: If I{size} is specified, the underlying
+ configFS object will be created with that parameter.
+ No RDDRStorageObject with the same I{name} can pre-exist in the
+ parent RDDRBackstore in that mode, or instanciation will fail.
+ - B{Lookup mode}: If I{size} is not set, then the RDDRStorageObject
+ will be bound to the existing configFS object in the parent
+ RDDRBackstore having the specified I{name}.
+ The underlying configFS object must already exist in that mode,
+ or instanciation will fail.
+
+ @param backstore: The parent backstore of the RDDRStorageObject.
+ @type backstore: RDDRBackstore
+ @param name: The name of the RDDRStorageObject.
+ @type name: string
+ @param size: The size of the ramdrive to create:
+ - If size is an int, it represents a number of bytes
+ - If size is a string, the following units can be used :
+ - I{B} or no unit present for bytes
+ - I{k}, I{K}, I{kB}, I{KB} for kB (kilobytes)
+ - I{m}, I{M}, I{mB}, I{MB} for MB (megabytes)
+ - I{g}, I{G}, I{gB}, I{GB} for GB (gigabytes)
+ - I{t}, I{T}, I{tB}, I{TB} for TB (terabytes)
+ Example: size="1MB" for a one megabytes storage object.
+ - Note that the size will be rounded to the closest 4096 Bytes
+ RAM pages count. For instance, a size of 100000 Bytes will be
+ rounded to 24 pages, really 98304 Bytes.
+ - The base value for kilo is 1024, aka 1kB = 1024B.
+ Strictly speaking, we use kiB, MiB, etc.
+ @type size: string or int
+ @param gen_wwn: Should we generate a T10 WWN Unit Serial ?
+ @type gen_wwn: bool
+ @return: A RDDRStorageObject object.
+ '''
+
+ if size is not None:
+ super(RDDRStorageObject, self).__init__(backstore, RDDRBackstore,
+ name, 'create')
+ try:
+ self._configure(size, gen_wwn)
+ except:
+ self.delete()
+ raise
+ else:
+ super(RDDRStorageObject, self).__init__(backstore, RDDRBackstore,
+ name, 'lookup')
+
+ def _configure(self, size, wwn):
+ self._check_self()
+ size = convert_human_to_bytes(size)
+ # convert to 4k pages
+ size = round(float(size)/4096)
+ if size == 0:
+ size = 1
+
+ self._control("rd_pages=%d" % size)
+ self._enable()
+ if wwn:
+ self.wwn = generate_wwn('unit_serial')
+
+ def _get_page_size(self):
+ self._check_self()
+ return int(self._parse_info("PAGES/PAGE_SIZE").split('*')[1])
+
+ def _get_pages(self):
+ self._check_self()
+ return int(self._parse_info("PAGES/PAGE_SIZE").split('*')[0])
+
+ def _get_size(self):
+ self._check_self()
+ size = self._get_page_size() * self._get_pages()
+ return size
+
+ # RDDRStorageObject public stuff
+
+ page_size = property(_get_page_size,
+ doc="Get the ramdisk page size.")
+ pages = property(_get_pages,
+ doc="Get the ramdisk number of pages.")
+ size = property(_get_size,
+ doc="Get the ramdisk size in bytes.")
+
+class RDMCPStorageObject(StorageObject):
+ '''
+ An interface to configFS storage objects for rd_mcp backstore.
+ '''
+
+ # RDMCPStorageObject private stuff
+
+ def __init__(self, backstore, name, size=None, gen_wwn=True):
+ '''
+ A RDMCPStorageObject can be instanciated in two ways:
+ - B{Creation mode}: If I{size} is specified, the underlying
+ configFS object will be created with that parameter.
+ No RDMCPStorageObject with the same I{name} can pre-exist in the
+ parent RDMCPBackstore in that mode, or instanciation will fail.
+ - B{Lookup mode}: If I{size} is not set, then the
+ RDMCPStorageObject will be bound to the existing configFS object
+ in the parent RDMCPBackstore having the specified I{name}.
+ The underlying configFS object must already exist in that mode,
+ or instanciation will fail.
+
+ @param backstore: The parent backstore of the RDMCPStorageObject.
+ @type backstore: RDMCPBackstore
+ @param name: The name of the RDMCPStorageObject.
+ @type name: string
+ @param size: The size of the ramdrive to create:
+ - If size is an int, it represents a number of bytes
+ - If size is a string, the following units can be used :
+ - B{B} or no unit present for bytes
+ - B{k}, B{K}, B{kB}, B{KB} for kB (kilobytes)
+ - B{m}, B{M}, B{mB}, B{MB} for MB (megabytes)
+ - B{g}, B{G}, B{gB}, B{GB} for GB (gigabytes)
+ - B{t}, B{T}, B{tB}, B{TB} for TB (terabytes)
+ Example: size="1MB" for a one megabytes storage object.
+ - Note that the size will be rounded to the closest 4096 Bytes
+ RAM pages count. For instance, a size of 100000 Bytes will be
+ rounded to 24 pages, really 98304 Bytes.
+ - The base value for kilo is 1024, aka 1kB = 1024B.
+ Strictly speaking, we use kiB, MiB, etc.
+ @type size: string or int
+ @param gen_wwn: Should we generate a T10 WWN Unit Serial ?
+ @type gen_wwn: bool
+ @return: A RDMCPStorageObject object.
+ '''
+
+ if size is not None:
+ super(RDMCPStorageObject, self).__init__(backstore,
+ RDMCPBackstore,
+ name,
+ 'create')
+ try:
+ self._configure(size, gen_wwn)
+ except:
+ self.delete()
+ raise
+ else:
+ super(RDMCPStorageObject, self).__init__(backstore,
+ RDMCPBackstore,
+ name,
+ 'lookup')
+
+ def _configure(self, size, wwn):
+ self._check_self()
+ size = convert_human_to_bytes(size)
+ # convert to 4k pages
+ size = round(float(size)/4096)
+ if size == 0:
+ size = 1
+
+ self._control("rd_pages=%d" % size)
+ self._enable()
+ if wwn:
+ self.wwn = generate_wwn('unit_serial')
+
+ def _get_page_size(self):
+ self._check_self()
+ return int(self._parse_info("PAGES/PAGE_SIZE").split('*')[1])
+
+ def _get_pages(self):
+ self._check_self()
+ return int(self._parse_info("PAGES/PAGE_SIZE").split('*')[0])
+
+ def _get_size(self):
+ self._check_self()
+ size = self._get_page_size() * self._get_pages()
+ return size
+
+ # RDMCPStorageObject public stuff
+
+ page_size = property(_get_page_size,
+ doc="Get the ramdisk page size.")
+ pages = property(_get_pages,
+ doc="Get the ramdisk number of pages.")
+ size = property(_get_size,
+ doc="Get the ramdisk size in bytes.")
+
+
+class FileIOStorageObject(StorageObject):
+ '''
+ An interface to configFS storage objects for fileio backstore.
+ '''
+
+ # FileIOStorageObject private stuff
+
+ def __init__(self, backstore, name, dev=None, size=None,
+ gen_wwn=True, buffered_mode=False):
+ '''
+ A FileIOStorageObject can be instanciated in two ways:
+ - B{Creation mode}: If I{dev} and I{size} are specified, the
+ underlying configFS object will be created with those parameters.
+ No FileIOStorageObject with the same I{name} can pre-exist in the
+ parent FileIOBackstore in that mode, or instanciation will fail.
+ - B{Lookup mode}: If I{dev} and I{size} are not set, then the
+ FileIOStorageObject will be bound to the existing configFS object
+ in the parent FileIOBackstore having the specified I{name}.
+ The underlying configFS object must already exist in that mode,
+ or instanciation will fail.
+
+ @param backstore: The parent backstore of the FileIOStorageObject.
+ @type backstore: FileIOBackstore
+ @param name: The name of the FileIOStorageObject.
+ @type name: string
+ @param dev: The path to the backend file or block device to be used.
+ - Examples: I{dev="/dev/sda"}, I{dev="/tmp/myfile"}
+ - The only block device type that is accepted I{TYPE_DISK}, or
+ partitions of a I{TYPE_DISK} device.
+ For other device types, use pscsi.
+ @type dev: string
+ @param size: The maximum size to allocate for the file.
+ Not used for block devices.
+ - If size is an int, it represents a number of bytes
+ - If size is a string, the following units can be used :
+ - B{B} or no unit present for bytes
+ - B{k}, B{K}, B{kB}, B{KB} for kB (kilobytes)
+ - B{m}, B{M}, B{mB}, B{MB} for MB (megabytes)
+ - B{g}, B{G}, B{gB}, B{GB} for GB (gigabytes)
+ - B{t}, B{T}, B{tB}, B{TB} for TB (terabytes)
+ Example: size="1MB" for a one megabytes storage object.
+ - The base value for kilo is 1024, aka 1kB = 1024B.
+ Strictly speaking, we use kiB, MiB, etc.
+ @type size: string or int
+ @param gen_wwn: Should we generate a T10 WWN Unit Serial ?
+ @type gen_wwn: bool
+ @param buffered_mode: Should we create the StorageObject in buffered
+ mode or not ? Byt default, we create it in synchronous mode
+ (non-buffered). This cannot be changed later.
+ @type buffered_mode: bool
+ @return: A FileIOStorageObject object.
+ '''
+
+ if dev is not None:
+ super(FileIOStorageObject, self).__init__(backstore,
+ FileIOBackstore,
+ name,
+ 'create')
+ try:
+ self._configure(dev, size, gen_wwn, buffered_mode)
+ except:
+ self.delete()
+ raise
+ else:
+ super(FileIOStorageObject, self).__init__(backstore,
+ FileIOBackstore,
+ name,
+ 'lookup')
+
+ def _configure(self, dev, size, wwn, buffered_mode):
+ self._check_self()
+ rdev = os.path.realpath(dev)
+ if not os.path.isdir(os.path.dirname(rdev)):
+ raise RTSLibError("The dev parameter must be a path to a "
+ + "file inside an existing directory, "
+ + "not %s." % str(os.path.dirname(dev)))
+ if os.path.isdir(rdev):
+ raise RTSLibError("The dev parameter must be a path to a "
+ + "file or block device not a directory:"
+ + "%s." % dev)
+
+ block_type = get_block_type(rdev)
+ if block_type is None and not is_disk_partition(rdev):
+ if os.path.exists(rdev) and not os.path.isfile(dev):
+ raise RTSLibError("Device %s is neither a file, " % dev
+ + "a disk partition or a block device.")
+ # It is a file
+ if size is None:
+ raise RTSLibError("The size parameter is mandatory "
+ + "when using a file.")
+ size = convert_human_to_bytes(size)
+ self._control("fd_dev_name=%s,fd_dev_size=%d" % (dev, size))
+ else:
+ # it is a block device or a disk partition
+ if size is not None:
+ raise RTSLibError("You cannot specify a size for a "
+ + "block device.")
+ if block_type != 0 and block_type is not None:
+ raise RTSLibError("Device %s is a block device, " % dev
+ + "but not of TYPE_DISK.")
+ if is_dev_in_use(rdev):
+ raise RTSLibError("Cannot configure StorageObject "
+ + "because device "
+ + "%s is already in use." % dev)
+ if is_disk_partition(rdev):
+ size = get_disk_size(rdev)
+ print "fd_dev_name=%s,fd_dev_size=%d" % (dev, size)
+ self._control("fd_dev_name=%s,fd_dev_size=%d" % (dev, size))
+ else:
+ self._control("fd_dev_name=%s" % dev)
+
+ self._set_udev_path(dev)
+
+ if buffered_mode:
+ self._set_buffered_mode()
+
+ self._enable()
+
+ if wwn:
+ self.wwn = generate_wwn('unit_serial')
+
+ def _get_mode(self):
+ self._check_self()
+ return self._parse_info('Mode')
+
+ def _get_size(self):
+ self._check_self()
+ return int(self._parse_info('Size'))
+
+ def _set_buffered_mode(self):
+ '''
+ FileIOStorage objects have synchronous mode enable by default.
+ This allows to move them to buffered mode.
+ Warning, setting the object back to synchronous mode is not
+ implemented yet, so there is no turning back unless you delete
+ and recreate the FileIOStorageObject.
+ '''
+ self._check_self()
+ self._control("fd_buffered_io=1")
+
+ # FileIOStorageObject public stuff
+
+ mode = property(_get_mode,
+ doc="Get the current FileIOStorage mode, buffered or synchronous")
+ size = property(_get_size,
+ doc="Get the current FileIOStorage size in bytes")
+
+class IBlockStorageObject(StorageObject):
+ '''
+ An interface to configFS storage objects for iblock backstore.
+ '''
+
+ # IBlockStorageObject private stuff
+
+ def __init__(self, backstore, name, dev=None, gen_wwn=True):
+ '''
+ A BlockIOStorageObject can be instanciated in two ways:
+ - B{Creation mode}: If I{dev} is specified, the underlying configFS
+ object will be created with that parameter.
+ No BlockIOStorageObject with the same I{name} can pre-exist in
+ the parent BlockIOBackstore in that mode.
+ - B{Lookup mode}: If I{dev} is not set, then the
+ BlockIOStorageObject will be bound to the existing configFS
+ object in the parent BlockIOBackstore having the specified
+ I{name}. The underlying configFS object must already exist in
+ that mode, or instanciation will fail.
+
+ @param backstore: The parent backstore of the BlockIOStorageObject.
+ @type backstore: BlockIOBackstore
+ @param name: The name of the BlockIOStorageObject.
+ @type name: string
+ @param dev: The path to the backend block device to be used.
+ - Example: I{dev="/dev/sda"}.
+ - The only device type that is accepted I{TYPE_DISK}.
+ For other device types, use pscsi.
+ @type dev: string
+ @param gen_wwn: Should we generate a T10 WWN Unit Serial when
+ creating the object ?
+ @type gen_wwn: bool
+ @return: A BlockIOStorageObject object.
+ '''
+
+ if dev is not None:
+ super(IBlockStorageObject, self).__init__(backstore,
+ IBlockBackstore,
+ name,
+ 'create')
+ try:
+ self._configure(dev, gen_wwn)
+ except:
+ self.delete()
+ raise
+ else:
+ super(IBlockStorageObject, self).__init__(backstore,
+ IBlockBackstore,
+ name,
+ 'lookup')
+
+ def _configure(self, dev, wwn):
+ self._check_self()
+ if get_block_type(dev) != 0:
+ raise RTSLibError("Device is not a TYPE_DISK block device.")
+ if is_dev_in_use(dev):
+ raise RTSLibError("Cannot configure StorageObject because "
+ + "device %s is already in use." % dev)
+ self._set_udev_path(dev)
+ if self._backstore.version.startswith("v3."):
+ # For 3.x, use the fd method
+ file_fd = os.open(dev, os.O_RDWR)
+ try:
+ self._write_fd(file_fd)
+ finally:
+ os.close(file_fd)
+ else:
+ # For 4.x and above, use the generic udev_path method
+ self._control("udev_path=%s" % dev)
+ self._enable()
+ if wwn:
+ self.wwn = generate_wwn('unit_serial')
+
+ def _get_major(self):
+ self._check_self()
+ return int(self._parse_info('Major'))
+
+ def _get_minor(self):
+ self._check_self()
+ return int(self._parse_info('Minor'))
+
+ # IblockStorageObject public stuff
+
+ major = property(_get_major,
+ doc="Get the block device major number")
+ minor = property(_get_minor,
+ doc="Get the block device minor number")
+
+def _test():
+ import doctest
+ doctest.testmod()
+
+if __name__ == "__main__":
+ _test()
diff --git a/rtslib/utils.py b/rtslib/utils.py
new file mode 100644
index 0000000..a10e43c
--- /dev/null
+++ b/rtslib/utils.py
@@ -0,0 +1,630 @@
+'''
+Provides various utility functions.
+
+This file is part of RTSLib Community Edition.
+Copyright (c) 2011 by RisingTide Systems LLC
+
+This program is free software: you can redistribute it and/or modify
+it under the terms of the GNU Affero General Public License as
+published by the Free Software Foundation, version 3 (AGPLv3).
+
+This program is distributed in the hope that it will be useful,
+but WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+GNU Affero General Public License for more details.
+
+You should have received a copy of the GNU Affero General Public License
+along with this program. If not, see <http://www.gnu.org/licenses/>.
+'''
+
+import re
+import os
+import stat
+import uuid
+import glob
+import socket
+import subprocess
+
+from array import array
+from fcntl import ioctl
+from struct import pack, unpack
+
+class RTSLibError(Exception):
+ '''
+ Generic rtslib error.
+ '''
+ pass
+
+class RTSLibBrokenLink(RTSLibError):
+ '''
+ Broken link in configfs, i.e. missing LUN storage object.
+ '''
+ pass
+
+def flatten_nested_list(nested_list):
+ '''
+ Function to flatten a nested list.
+
+ >>> import rtslib.utils as utils
+ >>> utils.flatten_nested_list([[1,2,3,[4,5,6]],[7,8],[[[9,10]],[11,]]])
+ [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11]
+
+ @param nested_list: A nested list (list of lists of lists etc.)
+ @type nested_list: list
+ @return: A list with only non-list elements
+ '''
+ return list(gen_list_item(nested_list))
+
+def gen_list_item(nested_list):
+ '''
+ The generator for flatten_nested_list().
+ It returns one by one items that are not a list, and recurses when
+ he finds an item that is a list.
+ '''
+ for item in nested_list:
+ if type(item) is list:
+ for nested_item in gen_list_item(item):
+ yield nested_item
+ else:
+ yield item
+
+def fwrite(path, string):
+ '''
+ This function writes a string to a file, and takes care of
+ opening it and closing it. If the file does not exists, it
+ will be created.
+
+ >>> from rtslib.utils import *
+ >>> fwrite("/tmp/test", "hello")
+ >>> fread("/tmp/test")
+ 'hello'
+
+ @param path: The file to write to.
+ @type path: string
+ @param string: The string to write to the file.
+ @type string: string
+
+ '''
+ path = os.path.realpath(str(path))
+ file_fd = open(path, 'w')
+ try:
+ file_fd.write("%s" % string)
+ finally:
+ file_fd.close()
+
+def fread(path):
+ '''
+ This function reads the contents of a file.
+ It takes care of opening and closing it.
+
+ >>> from rtslib.utils import *
+ >>> fwrite("/tmp/test", "hello")
+ >>> fread("/tmp/test")
+ 'hello'
+ >>> fread("/tmp/notexistingfile") # doctest: +ELLIPSIS
+ Traceback (most recent call last):
+ ...
+ IOError: [Errno 2] No such file or directory: '/tmp/notexistingfile'
+
+ @param path: The path to the file to read from.
+ @type path: string
+ @return: A string containing the file's contents.
+
+ '''
+ path = os.path.realpath(str(path))
+ string = ""
+ file_fd = open(path, 'r')
+ try:
+ string = file_fd.read()
+ finally:
+ file_fd.close()
+
+ return string
+
+def is_dev_in_use(path):
+ '''
+ This function will check if the device or file referenced by path is
+ already mounted or used as a storage object backend. It works by trying to
+ open the path with O_EXCL flag, which will fail if someone else already
+ did. Note that the file is closed before the function returns, so this
+ does not guaranteed the device will still be available after the check.
+ @param path: path to the file of device to check
+ @type path: string
+ @return: A boolean, True is we cannot get exclusive descriptor on the path,
+ False if we can.
+ '''
+ path = os.path.realpath(str(path))
+ try:
+ file_fd = os.open(path, os.O_EXCL|os.O_NDELAY)
+ except OSError:
+ return True
+ else:
+ os.close(file_fd)
+ return False
+
+def is_disk_partition(path):
+ '''
+ Try to find out if path is a partition of a TYPE_DISK device.
+ Handles both /dev/sdaX and /dev/disk/by-*/*-part? schemes.
+ '''
+ regex = re.match(r'([a-z/]+)([1-9]+)$', path)
+ if not regex:
+ regex = re.match(r'(/dev/disk/.+)(-part[1-9]+)$', path)
+ if not regex:
+ return False
+ else:
+ if get_block_type(regex.group(1)) == 0:
+ return True
+
+def get_disk_size(path):
+ '''
+ This function returns the size in bytes of a disk-type
+ block device, or None if path does not point to a disk-
+ type device.
+ '''
+ (major, minor) = get_block_numbers(path)
+ if major is None:
+ return None
+ # list of [major, minor, #blocks (1K), name
+ partitions = [ x.split()[0:4]
+ for x in fread("/proc/partitions").split("\n")[2:] if x]
+ size = None
+ for partition in partitions:
+ if partition[0:2] == [str(major), str(minor)]:
+ size = int(partition[2]) * 1024
+ break
+ return size
+
+def get_block_numbers(path):
+ '''
+ This function returns a (major,minor) tuple for the block
+ device found at path, or (None, None) if path is
+ not a block device.
+ '''
+ dev = os.path.realpath(path)
+ try:
+ mode = os.stat(dev)
+ except OSError:
+ return (None, None)
+
+ if not stat.S_ISBLK(mode[stat.ST_MODE]):
+ return (None, None)
+
+ major = os.major(mode.st_rdev)
+ minor = os.minor(mode.st_rdev)
+ return (major, minor)
+
+def get_block_type(path):
+ '''
+ This function returns a block device's type.
+ Example: 0 is TYPE_DISK
+ If no match is found, None is returned.
+
+ >>> from rtslib.utils import *
+ >>> get_block_type("/dev/sda")
+ 0
+ >>> get_block_type("/dev/sr0")
+ 5
+ >>> get_block_type("/dev/scd0")
+ 5
+ >>> get_block_type("/dev/nodevicehere") is None
+ True
+
+ @param path: path to the block device
+ @type path: string
+ @return: An int for the block device type, or None if not a block device.
+ '''
+ dev = os.path.realpath(path)
+ # TODO: Make adding new majors on-the-fly possible, using some config file
+ # for instance, maybe an additionnal list argument, or even a match all
+ # mode for overrides ?
+
+ # Make sure we are dealing with a block device
+ (major, minor) = get_block_numbers(dev)
+ if major is None:
+ return None
+
+ # Treat disk partitions as TYPE_DISK
+ if is_disk_partition(path):
+ return 0
+
+ # Assume that the assigned experimental major range devices are TYPE_DISK
+ if 239 < major < 255:
+ return 0
+
+ # DRBD devices do not report type but can be treated as TYPE_DISK
+ if major == 147:
+ return 0
+
+ # TODO: This should no be there as block device 30 is normally
+ # 'Philips LMS CM-205 CD-ROM' in the Linux devices list
+ # Cirtas devices do not report type but can be treated as TYPE_DISK
+ if major == 30:
+ return 0
+
+ # Same for LVM LVs, but as we cannot use major here
+ # (it varies accross distros), use the realpath to check
+ if os.path.dirname(dev) == "/dev/mapper":
+ return 0
+
+ # list of (major, minor, type) tuples
+ blocks = [(fread("%s/dev" % fdev).strip().split(':')[0],
+ fread("%s/dev" % fdev).strip().split(':')[1],
+ fread("%s/device/type" % fdev).strip())
+ for fdev in glob.glob("/sys/block/*")
+ if os.path.isfile("%s/device/type" % fdev)]
+
+ for block in blocks:
+ if int(block[0]) == major and int(block[1]) == minor:
+ return int(block[2])
+
+ return None
+
+def list_scsi_hbas():
+ '''
+ This function returns the list of HBA indexes for existing SCSI HBAs.
+ '''
+ return list(set([int(device.partition(":")[0])
+ for device in os.listdir("/sys/bus/scsi/devices")
+ if re.match("[0-9:]+", device)]))
+
+def convert_scsi_path_to_hctl(path):
+ '''
+ This function returns the SCSI ID in H:C:T:L form for the block
+ device being mapped to the udev path specified.
+ If no match is found, None is returned.
+
+ >>> import rtslib.utils as utils
+ >>> utils.convert_scsi_path_to_hctl('/dev/scd0')
+ (2, 0, 0, 0)
+ >>> utils.convert_scsi_path_to_hctl('/dev/sr0')
+ (2, 0, 0, 0)
+ >>> utils.convert_scsi_path_to_hctl('/dev/sda')
+ (3, 0, 0, 0)
+ >>> utils.convert_scsi_path_to_hctl('/dev/sda1')
+ >>> utils.convert_scsi_path_to_hctl('/dev/sdb')
+ (3, 0, 1, 0)
+ >>> utils.convert_scsi_path_to_hctl('/dev/sdc')
+ (3, 0, 2, 0)
+
+ @param path: The udev path to the SCSI block device.
+ @type path: string
+ @return: An (host, controller, target, lun) tuple of integer
+ values representing the SCSI ID of the device, or None if no
+ match is found.
+ '''
+ dev = os.path.realpath(path)
+ scsi_devices = [os.path.basename(scsi_dev).split(':')
+ for scsi_dev in glob.glob("/sys/class/scsi_device/*")]
+ for (host, controller, target, lun) in scsi_devices:
+ scsi_dev = convert_scsi_hctl_to_path(host, controller, target, lun)
+ if dev == scsi_dev:
+ return (int(host), int(controller), int(target), int(lun))
+
+ return None
+
+def convert_scsi_hctl_to_path(host, controller, target, lun):
+ '''
+ This function returns a udev path pointing to the block device being
+ mapped to the SCSI device that has the provided H:C:T:L.
+
+ >>> import rtslib.utils as utils
+ >>> utils.convert_scsi_hctl_to_path(0,0,0,0)
+ ''
+ >>> utils.convert_scsi_hctl_to_path(2,0,0,0) # doctest: +ELLIPSIS
+ '/dev/s...0'
+ >>> utils.convert_scsi_hctl_to_path(3,0,2,0)
+ '/dev/sdc'
+
+ @param host: The SCSI host id.
+ @type host: int
+ @param controller: The SCSI controller id.
+ @type controller: int
+ @param target: The SCSI target id.
+ @type target: int
+ @param lun: The SCSI Logical Unit Number.
+ @type lun: int
+ @return: A string for the canonical path to the device, or empty string.
+ '''
+ try:
+ host = int(host)
+ controller = int(controller)
+ target = int(target)
+ lun = int(lun)
+ except ValueError:
+ raise RTSLibError(
+ "The host, controller, target and lun parameter must be integers.")
+
+ scsi_dev_path = "/sys/class/scsi_device"
+ sysfs_names = [os.path.basename(name) for name
+ in glob.glob("%s/%d:%d:%d:%d/device/block:*"
+ % (scsi_dev_path, host, controller, target, lun))]
+ if len(sysfs_names) == 0:
+ sysfs_names = [os.path.basename(name) for name
+ in glob.glob("%s/%d:%d:%d:%d/device/block/*"
+ % (scsi_dev_path, host, controller, target, lun))]
+ if len(sysfs_names) > 0:
+ for name in sysfs_names:
+ name1 = name.partition(":")[2].strip()
+ if name1:
+ name = name1
+ dev = os.path.realpath("/dev/%s" % name)
+ try:
+ mode = os.stat(dev)[stat.ST_MODE]
+ except OSError:
+ pass
+ if stat.S_ISBLK(mode):
+ return dev
+ else:
+ return ''
+
+def convert_human_to_bytes(hsize, kilo=1024):
+ '''
+ This function converts human-readable amounts of bytes to bytes.
+ It understands the following units :
+ - I{B} or no unit present for Bytes
+ - I{k}, I{K}, I{kB}, I{KB} for kB (kilobytes)
+ - I{m}, I{M}, I{mB}, I{MB} for MB (megabytes)
+ - I{g}, I{G}, I{gB}, I{GB} for GB (gigabytes)
+ - I{t}, I{T}, I{tB}, I{TB} for TB (terabytes)
+
+ Note: The definition of I{kilo} defaults to 1kB = 1024Bytes.
+ Strictly speaking, those should not be called I{kB} but I{kiB}.
+ You can override that with the optional kilo parameter.
+
+ Example:
+
+ >>> import rtslib.utils as utils
+ >>> utils.convert_human_to_bytes("1k")
+ 1024
+ >>> utils.convert_human_to_bytes("1k", 1000)
+ 1000
+ >>> utils.convert_human_to_bytes("1MB")
+ 1048576
+ >>> utils.convert_human_to_bytes("12kB")
+ 12288
+
+ @param hsize: The human-readable version of the Bytes amount to convert
+ @type hsize: string or int
+ @param kilo: Optionnal base for the kilo prefix
+ @type kilo: int
+ @return: An int representing the human-readable string converted to bytes
+ '''
+ size = str(hsize).replace("g","G").replace("K","k")
+ size = size.replace("m","M").replace("t","T")
+ if not re.match("^[0-9]+[T|G|M|k]?[B]?$", size):
+ raise RTSLibError("Cannot interpret size, wrong format: %s" % hsize)
+
+ size = size.rstrip('B')
+
+ units = ['k', 'M', 'G', 'T']
+ try:
+ power = units.index(size[-1]) + 1
+ except ValueError:
+ power = 0
+ size = int(size)
+ else:
+ size = int(size[:-1])
+
+ size = size * int(kilo) ** power
+ return size
+
+def generate_wwn(wwn_type):
+ '''
+ Generates a random WWN of the specified type:
+ - unit_serial: T10 WWN Unit Serial.
+ - iqn: iSCSI IQN
+ - naa: SAS NAA address
+ @param wwn_type: The WWN address type.
+ @type wwn_type: str
+ @returns: A string containing the WWN.
+ '''
+ wwn_type = wwn_type.lower()
+ if wwn_type == 'free':
+ return str(uuid.uuid4())
+ if wwn_type == 'unit_serial':
+ return str(uuid.uuid4())
+ elif wwn_type == 'iqn':
+ localname = socket.gethostname().split(".")[0]
+ localarch = os.uname()[4].replace("_","")
+ prefix = "iqn.2003-01.org.linux-iscsi.%s.%s" % (localname, localarch)
+ prefix = prefix.strip().lower()
+ serial = "sn.%s" % str(uuid.uuid4())[24:]
+ return "%s:%s" % (prefix, serial)
+ elif wwn_type == 'naa':
+ sas_address = "naa.6001405%s" % str(uuid.uuid4())[:10]
+ return sas_address.replace('-', '')
+ else:
+ raise ValueError("Unknown WWN type: %s." % wwn_type)
+
+def is_valid_wwn(wwn_type, wwn, wwn_list=None):
+ '''
+ Returns True if the wwn is a valid wwn of type wwn_type.
+ @param wwn_type: The WWN address type.
+ @type wwn_type: str
+ @param wwn: The WWN address to check.
+ @type wwn: str
+ @param wwn_list: An optional list of wwns to check the wwn parameter from.
+ @type wwn_list: list of str
+ @returns: bool.
+ '''
+ wwn_type = wwn_type.lower()
+
+ if wwn_list is not None and wwn not in wwn_list:
+ return False
+ elif wwn_type == 'free':
+ return True
+ elif wwn_type == 'iqn' \
+ and re.match("iqn\.[0-9]{4}-[0-1][0-9]\..*\..*", wwn) \
+ and not re.search(' ', wwn) \
+ and not re.search('_', wwn):
+ return True
+ elif wwn_type == 'naa' \
+ and re.match("naa\.[0-9A-Fa-f]{16}$", wwn):
+ return True
+ elif wwn_type == 'unit_serial' \
+ and re.match(
+ "[0-9A-Fa-f]{8}(-[0-9A-Fa-f]{4}){3}-[0-9A-Fa-f]{12}$", wwn):
+ return True
+ else:
+ return False
+
+def list_available_kernel_modules():
+ '''
+ List all loadable kernel modules as registered by depmod
+ '''
+ kver = os.uname()[2]
+ depfile = "/lib/modules/%s/modules.dep" % kver
+ return [module.split(".")[0] for module in
+ re.findall(r"[a-zA-Z0-9_-]+\.ko:", fread(depfile))]
+
+def list_loaded_kernel_modules():
+ '''
+ List all currently loaded kernel modules
+ '''
+ return [line.split(" ")[0] for line in
+ fread("/proc/modules").split('\n') if line]
+
+def modprobe(module):
+ '''
+ Load the specified kernel module if needed.
+ @param module: The name of the kernel module to be loaded.
+ @type module: str
+ @return: Whether of not we had to load the module.
+ '''
+ if module not in list_loaded_kernel_modules():
+ if module in list_available_kernel_modules():
+ try:
+ exec_argv(["modprobe", module])
+ except:
+ raise RTSLibError("Kernel module %s exists "
+ % module + "but fails to load: %s")
+ else:
+ return True
+ else:
+ raise RTSLibError("Kernel module %s does not exists on disk "
+ % module + "and is not loaded.")
+ else:
+ return False
+
+def exec_argv(argv, strip=True, shell=False):
+ '''
+ Executes a command line given as an argv table and either:
+ - raise an exception if return != 0
+ - return the output
+ If strip is True, then output lines will be stripped.
+ If shell is True, the argv must be a string that will be evaluated by the
+ shell, instead of the argv list.
+
+ '''
+ process = subprocess.Popen(argv,
+ stdout=subprocess.PIPE,
+ stderr=subprocess.PIPE,
+ shell=shell)
+ (stdoutdata, stderrdata) = process.communicate()
+ # Remove indents, trailing space and empty lines in output.
+ if strip:
+ stdoutdata = "\n".join([line.strip()
+ for line in stdoutdata.split("\n")
+ if line.strip()])
+ stderrdata = "\n".join([line.strip()
+ for line in stderrdata.split("\n")
+ if line.strip()])
+ if process.returncode != 0:
+ raise RTSLibError(stderrdata)
+ else:
+ return stdoutdata
+
+def list_eth_names(max_eth=1024):
+ '''
+ List the max_eth first local ethernet interfaces names from SIOCGIFCONF
+ struct.
+ '''
+ SIOCGIFCONF = 0x8912
+ if os.uname()[4].endswith("_64"):
+ offset = 40
+ else:
+ offset = 32
+ bytes = 32 * max_eth
+ sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
+ ifaces = array('B', '\0' * bytes)
+ packed = pack('iL', bytes, ifaces.buffer_info()[0])
+ outbytes = unpack('iL', ioctl(sock.fileno(), SIOCGIFCONF, packed))[0]
+ names = ifaces.tostring()
+ return [names[i:i+offset].split('\0', 1)[0]
+ for i in range(0, outbytes, offset)]
+
+def list_eth_ips(ifnames=None):
+ '''
+ List the IP addresses of a list of ethernet interfaces from the SIOCGIFADDR
+ struct. If ifname is omitted, list all IPs of all ifaces excepted for lo.
+ '''
+ SIOCGIFADDR = 0x8915
+ if ifnames is None:
+ ifnames = [iface for iface in list_eth_names() if iface != 'lo']
+ ips = []
+ for ifname in ifnames:
+ sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
+ packed = pack('256s', ifname[:15])
+ ips.append(socket.inet_ntoa(ioctl(sock.fileno(),
+ SIOCGIFADDR,
+ packed)[20:24]))
+ return flatten_nested_list(ips)
+
+def get_main_ip():
+ '''
+ Try to guess the local machine non-loopback IP.
+ If available, local hostname resolution is used (if non-loopback),
+ else try to find an other non-loopback IP on configured NICs.
+ If no usable IP address is found, returns None.
+ '''
+ # socket.gethostbyname does no have a timeout parameter
+ # Let's use a thread to implement that in the background
+ from threading import Thread
+ from Queue import Queue, Empty
+
+ def start_thread(func):
+ thread = Thread(target = func)
+ thread.setDaemon(True)
+ thread.start()
+
+ def gethostbyname_timeout(hostname, timeout = 1):
+ queue = Queue(1)
+
+ def try_gethostbyname(hostname):
+ try:
+ hostname = socket.gethostbyname(hostname)
+ except socket.gaierror:
+ hostname = None
+ return hostname
+
+ def queue_try_gethostbyname():
+ queue.put(try_gethostbyname(hostname))
+
+ start_thread(queue_try_gethostbyname)
+
+ try:
+ result = queue.get(block = True, timeout = timeout)
+ except Empty:
+ result = None
+ return result
+
+ local_ips = list_eth_ips()
+ # try to get a resolution in less than 1 second
+ host_ip = gethostbyname_timeout(socket.gethostname())
+ # Put the host IP in first position of the IP list if it exists
+ if host_ip in local_ips:
+ local_ips.remove(host_ip)
+ local_ips.insert(0, host_ip)
+ for ip_addr in local_ips:
+ if not ip_addr.startswith("127.") and ip_addr.strip():
+ return ip_addr
+ return None
+
+def _test():
+ '''Run the doctests'''
+ import doctest
+ doctest.testmod()
+
+if __name__ == "__main__":
+ _test()
diff --git a/setup.py b/setup.py
new file mode 100755
index 0000000..07e0bd6
--- /dev/null
+++ b/setup.py
@@ -0,0 +1,41 @@
+#! /usr/bin/env python
+'''
+This file is part of RTSLib Community Edition.
+Copyright (c) 2011 by RisingTide Systems LLC
+
+This program is free software: you can redistribute it and/or modify
+it under the terms of the GNU Affero General Public License as
+published by the Free Software Foundation, version 3 (AGPLv3).
+
+This program is distributed in the hope that it will be useful,
+but WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+GNU Affero General Public License for more details.
+
+You should have received a copy of the GNU Affero General Public License
+along with this program. If not, see <http://www.gnu.org/licenses/>.
+'''
+
+import re
+from distutils.core import setup
+import rtslib
+
+PKG = rtslib
+VERSION = str(PKG.__version__)
+(AUTHOR, EMAIL) = re.match('^(.*?)\s*<(.*)>$', PKG.__author__).groups()
+URL = PKG.__url__
+LICENSE = PKG.__license__
+SCRIPTS = []
+DESCRIPTION = PKG.__description__
+
+setup(
+ name=PKG.__name__,
+ description=DESCRIPTION,
+ version=VERSION,
+ author=AUTHOR,
+ author_email=EMAIL,
+ license=LICENSE,
+ url=URL,
+ scripts=SCRIPTS,
+ packages=[PKG.__name__],
+ package_data = {'':[]})
diff --git a/specs/README b/specs/README
new file mode 100644
index 0000000..552c39e
--- /dev/null
+++ b/specs/README
@@ -0,0 +1,137 @@
+# This file is part of RTSLib Community Edition.
+# Copyright (c) 2011 by RisingTide Systems LLC
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU Affero General Public License as
+# published by the Free Software Foundation, version 3 (AGPLv3).
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU Affero General Public License for more details.
+#
+# You should have received a copy of the GNU Affero General Public License
+# along with this program. If not, see <http://www.gnu.org/licenses/>.
+
+This directory (normally /var/lib/target) contains the spec files for
+RisingTide Systems's LIO SCSI target subsystem fabric modules.
+
+Each spec file should be named MODULE.spec, where MODULE is the name the fabric
+module is to be referred as. It contains a series of KEY = VALUE pairs, one per
+line.
+
+KEY is an alphanumeric (no spaces) string.
+VALUE can be anything. Quotes can be used for strings, but are not mandatory.
+Lists of VALUEs are comma-separated.
+
+Syntax
+------
+
+* Strings
+String values can either be enclosed in double quotes or not.
+Those examples are equivalent:
+kernel_module = "my_module"
+kernel_module = my_module
+
+* Lists
+Lists are comma-separated lists of values. If you want to use a comma in a
+string, use double quotes. Example:
+my_string = value1, value2, "value3, with comma", value4
+
+* Comments
+All lines beginning with a pound sign (#) will be ignored.
+Empty lines will be ignored too.
+
+Available keys
+--------------
+
+* features
+Lists the target fabric available features. Default value:
+discovery_auth, acls, acls_auth, nps
+exemple: features = discovery_auth, acls, acls_auth
+
+Detail of features:
+
+ * tpgts
+ The target fabric module is using iSCSI-style target portal group tags.
+
+ * discovery_auth
+ The target fabric module supports a fabric-wide authentication for
+ discovery.
+
+ * acls
+ The target's TPGTs do support explicit initiator ACLs.
+
+ * acls_auth
+ The target's TPGT's ACLs do support per-ACL initiator authentication.
+
+ * nps
+ The TPGTs do support iSCSI-like IPv4/IPv6 network portals, using IP:PORT
+ group names.
+
+ * nexus
+ The TPGTs do have a 'nexus' attribute that contains the local initiator
+ serial unit. This attribute must be set before being able to create any
+ LUNs.
+
+* wwn_type
+Sets the type of WWN expected by the target fabric. Defaults to 'free'.
+Example: wwn_type = iqn
+Current valid types are:
+
+ * free
+ Freeform WWN.
+
+ * iqn
+ The fabric module targets are using iSCSI-type IQNs.
+
+ * naa
+ NAA SAS address type WWN.
+
+ * unit_serial
+ Disk-type unit serial.
+
+* wwn_from_files
+In some cases, and independently from the wwn type, the target WWNs must be
+picked from a list of existing ones, the most obvious case being hardware-set
+WWNs. Only the WWNs both matching the wwn_type (after filtering if set, see
+below) and fetched from the specified files will be allowed for targets. The
+value of this key is a list (one or more, comma-separated) of UNIX style
+pathname patterns: * and ? wildcards can be used, and character ranges
+expressed with [] will be correctly expanded. Each file is assumed to contain
+one or more WWNs, and line ends, spaces, tabs and null (\0) will be considered
+as separators chars.
+Example: wwn_from_files = /sys/class/fc_host/host[0-9]/port_name
+
+* wwn_from_files_filter
+Empty by default, this one allows specifying a shell command to which each WWN
+from files will be fed, and the output of the filter will be used as the final
+WWN to use. Examples:
+wwn_from_files_filter = "sed -e s/0x// -e 's/../&:/g' -e s/:$//"
+wwn_from_files_filter = "sed -e s/0x// -e 's/../&:/g' -e s/:$// | tr [a-z] [A-Z]"
+The first example transforms strings like '0x21000024ff314c48' into
+'21:00:00:24:ff:31:4c:48', the second one also shifts lower cases into upper
+case, demonstrating that you can pipe as many commands you want into another.
+
+* wwn_from_cmds
+Same as wwn_from_files, but instead of taking a list of file patterns, takes a
+list of shell commands. Each commands output will be considered as a list of
+WWNs to be used, separated ny line ends, spaces, tabs and null (\0)
+chararcters.
+
+* wwn_from_cmds_filter
+Same as wwn_from_files_filter, but filters/transforms the WWNs gotten from the
+results of the wwn_from_cmds shell commands.
+
+* kernel_module
+Sets the name of the kernel module implementing the fabric modules. If not
+specified, it will be assumed to be MODULE_target_mod, where MODNAME is the
+name of the fabric module, as used to name the spec file. Note that you must
+not specify any .ko or such extension here.
+Example: kernel_module = my_module
+
+* configfs_group
+Sets the name of the configfs group used by the fabric module. Defaults to the
+name of the module as used to name the spec file.
+Example: configfs_group = iscsi
+
diff --git a/specs/example.spec.txt b/specs/example.spec.txt
new file mode 100644
index 0000000..9579893
--- /dev/null
+++ b/specs/example.spec.txt
@@ -0,0 +1,29 @@
+# Example LIO target fabric module.
+#
+# This file is part of RTSLib Community Edition.
+# Copyright (c) 2011 by RisingTide Systems LLC
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU Affero General Public License as
+# published by the Free Software Foundation, version 3 (AGPLv3).
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU Affero General Public License for more details.
+#
+# You should have received a copy of the GNU Affero General Public License
+# along with this program. If not, see <http://www.gnu.org/licenses/>.
+
+# The example fabric module uses the default feature set.
+# features = discovery_auth, acls, acls_auth, nps
+
+# This module uses anything as WWNs.
+wwn_type = free
+
+# Convoluted kernel module name. Default would be example_target_mod
+kernel_module = my_complex_kernel_module_name
+
+# The configfs group name. Defauklt would be "example"
+configfs_group = "example_group"
+
diff --git a/specs/ib_srpt.spec b/specs/ib_srpt.spec
new file mode 100644
index 0000000..e42eec9
--- /dev/null
+++ b/specs/ib_srpt.spec
@@ -0,0 +1,32 @@
+# The ib_srpt fabric module specfile.
+#
+# This file is part of RTSLib Community Edition.
+# Copyright (c) 2011 by RisingTide Systems LLC
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU Affero General Public License as
+# published by the Free Software Foundation, version 3 (AGPLv3).
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU Affero General Public License for more details.
+#
+# You should have received a copy of the GNU Affero General Public License
+# along with this program. If not, see <http://www.gnu.org/licenses/>.
+
+# The fabric module feature set
+features = acls
+
+# Non-standard module naming scheme
+kernel_module = ib_srpt
+
+# The module uses hardware addresses from there
+wwn_from_files = /sys/class/infiniband/*/ports/*/gids/0
+# Transform 'fe80:0000:0000:0000:0002:1903:000e:8acd' WWN notation to
+# '0x00000000000000000002c903000e8acd'
+wwn_from_files_filter = "sed -e s/fe80/0x0000/ -e 's/\://g'"
+
+# The configfs group
+configfs_group = srpt
+
diff --git a/specs/iscsi.spec b/specs/iscsi.spec
new file mode 100644
index 0000000..dd03319
--- /dev/null
+++ b/specs/iscsi.spec
@@ -0,0 +1,29 @@
+# The iscsi fabric module specfile.
+#
+# This file is part of RTSLib Community Edition.
+# Copyright (c) 2011 by RisingTide Systems LLC
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU Affero General Public License as
+# published by the Free Software Foundation, version 3 (AGPLv3).
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU Affero General Public License for more details.
+#
+# You should have received a copy of the GNU Affero General Public License
+# along with this program. If not, see <http://www.gnu.org/licenses/>.
+
+# The iscsi fabric module features set.
+features = discovery_auth, acls, acls_auth, nps, tpgts
+
+# Obviously, this module uses IQN strings as WWNs.
+wwn_type = iqn
+
+# This is default too
+# kernel_module = iscsi_target_mod
+
+# The configfs group name, default too
+# configfs_group = iscsi
+
diff --git a/specs/loopback.spec b/specs/loopback.spec
new file mode 100644
index 0000000..afead2e
--- /dev/null
+++ b/specs/loopback.spec
@@ -0,0 +1,28 @@
+# The tcm_loop fabric module specfile.
+#
+# This file is part of RTSLib Community Edition.
+# Copyright (c) 2011 by RisingTide Systems LLC
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU Affero General Public License as
+# published by the Free Software Foundation, version 3 (AGPLv3).
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU Affero General Public License for more details.
+#
+# You should have received a copy of the GNU Affero General Public License
+# along with this program. If not, see <http://www.gnu.org/licenses/>.
+
+# The fabric module feature set
+features = nexus
+
+# Use naa WWNs.
+wwn_type = naa
+
+# Non-standard module naming scheme
+kernel_module = tcm_loop
+
+# The configfs group
+configfs_group = loopback
diff --git a/specs/qla2xxx.spec b/specs/qla2xxx.spec
new file mode 100644
index 0000000..d47dcfa
--- /dev/null
+++ b/specs/qla2xxx.spec
@@ -0,0 +1,28 @@
+# The qla2xxx fabric module specfile.
+#
+# This file is part of RTSLib Community Edition.
+# Copyright (c) 2011 by RisingTide Systems LLC
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU Affero General Public License as
+# published by the Free Software Foundation, version 3 (AGPLv3).
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU Affero General Public License for more details.
+#
+# You should have received a copy of the GNU Affero General Public License
+# along with this program. If not, see <http://www.gnu.org/licenses/>.
+
+# The qla2xxx fabric module feature set
+features = acls
+
+# Non-standard module naming scheme
+kernel_module = tcm_qla2xxx
+
+# The module uses hardware addresses from there
+wwn_from_files = /sys/class/fc_host/host*/port_name
+
+# Transform '0x1234567812345678' WWN notation to '12:34:56:78:12:34:56:78'
+wwn_from_files_filter = "sed -e s/0x// -e 's/../&:/g' -e s/:$//"
diff --git a/specs/tcm_fc.spec b/specs/tcm_fc.spec
new file mode 100644
index 0000000..ad30974
--- /dev/null
+++ b/specs/tcm_fc.spec
@@ -0,0 +1,30 @@
+# The tcm_fc fabric module specfile.
+#
+# This file is part of RTSLib Community Edition.
+# Copyright (c) 2011 by RisingTide Systems LLC
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU Affero General Public License as
+# published by the Free Software Foundation, version 3 (AGPLv3).
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU Affero General Public License for more details.
+#
+# You should have received a copy of the GNU Affero General Public License
+# along with this program. If not, see <http://www.gnu.org/licenses/>.
+
+# The fabric module feature set
+features = acls
+
+# Non-standard module naming scheme
+kernel_module = tcm_fc
+
+# The module uses hardware addresses from there
+wwn_from_files = /sys/class/fc_host/host*/port_name
+# Transform '0x1234567812345678' WWN notation to '12:34:56:78:12:34:56:78'
+wwn_from_files_filter = "sed -e s/0x// -e 's/../&:/g' -e s/:$//"
+
+# The configfs group
+configfs_group = fc