diff options
author | Nicholas Bellinger <nab@risingtidesystems.com> | 2011-05-04 21:00:00 +0000 |
---|---|---|
committer | Nicholas Bellinger <nab@risingtidesystems.com> | 2011-05-04 21:00:00 +0000 |
commit | a0a62b501e314a607afb331c65f40772b5c4e464 (patch) | |
tree | 1bcafd25d9bd2d8be45e0cb621da57954782148f | |
download | rtslib-fb-1.90.tar.gz |
Initial rtslib commit1.90
Signed-off-by: Nicholas A. Bellinger <nab@risingtidesystems.com>
35 files changed, 5202 insertions, 0 deletions
@@ -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 + @@ -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 |